Pillowで画像処理を自動化:数千枚のファイルを一括リサイズ・透かし入れする秘訣

Python tutorial - IT technology blog
Python tutorial - IT technology blog

なぜ雑務の自動化にPillowを選んだのか?

Pythonは、デプロイスクリプトの実行からサーバーのアラート受信まで、繰り返しのタスクを処理してくれる私の「右腕」のような存在です。ある時、ECサイト向けに5万枚を超える膨大な画像を処理しなければならないことがありました。Photoshopで1枚ずつファイルを開いてリサイズし、ロゴを入れる作業はまさに苦行でした。半年間、本番環境でスクリプトを稼働させ続けた結果、Pillow(PILのフォーク)こそがこの混沌とした状況を解決する最も安定したライブラリであると確信しました。

このライブラリは、ファイルの読み書きが非常に高速なだけでなく、Linuxサーバー上でも極めて安定して動作します。ここでは、半年間システムを円滑に稼働させ続けるために私が使用した「テクニック」を紹介します。

クイックスタート:一瞬で終わる基本的な画像処理

まずはライブラリをインストールしましょう。ターミナルを開いて次のように入力します:

pip install Pillow

こちらは、任意の画像を「縮小」するための即実行可能なコードです:

from PIL import Image

# 画像ファイルを開く
with Image.open("input.jpg") as img:
    # 800x600のサイズにリサイズ
    resized_img = img.resize((800, 600))
    resized_img.save("output.jpg")

print("完了!あっという間です。")

一見シンプルですが、実際のサーバーでスクリプトを「完璧に」動作させ、画像を劣化させないためには、いくつかの技術的な注意点があります。

実戦で学んだ「秘伝」のテクニック解説

1. アスペクト比を維持したリサイズ(歪み防止)

resize() 関数を使用して固定の数値を渡すと、比率が崩れて画像が「横長」や「縦長」に歪んでしまいがちです。信じてください、クライアントはこれを嫌がります。代わりに、私は常に thumbnail() メソッドを使用することを推奨しています。

この関数は画像をその場でリサイズし、元の比率を維持したまま指定した枠内に収まるよう自動的に計算してくれます:

with Image.open("landscape.jpg") as img:
    max_size = (1200, 1200)
    img.thumbnail(max_size, Image.Resampling.LANCZOS)
    img.save("landscape_optimized.jpg")

豆知識: 常に Image.Resampling.LANCZOS を使用しましょう。処理に数ミリ秒余計にかかりますが、出力される画質は NEAREST のような簡易的なフィルタよりも遥かに鮮明です。

2. ウォーターマーク(透かし)の挿入:著作権の一括保護

透明度(透過)を維持したままロゴを画像に挿入するには、アルファチャンネルを理解する必要があります。以下のコードは、元の画像のサイズに関わらず、ロゴが常に右下に一定の余白を持って配置されるように計算されています。

def add_watermark(main_image_path, watermark_path, output_path):
    main_img = Image.open(main_image_path).convert("RGBA")
    watermark = Image.open(watermark_path).convert("RGBA")
    
    # ロゴの幅をメイン画像の約20%にリサイズ
    w_width, w_height = watermark.size
    main_width, main_height = main_img.size
    new_width = int(main_width * 0.2)
    new_height = int(w_height * (new_width / w_width))
    watermark = watermark.resize((new_width, new_height), Image.Resampling.LANCZOS)

    # 右下の位置、マージン20px
    position = (main_width - new_width - 20, main_height - new_height - 20)

    overlay = Image.new("RGBA", main_img.size, (0, 0, 0, 0))
    overlay.paste(watermark, position)

    # レイヤーを合成して保存
    combined = Image.alpha_composite(main_img, overlay)
    combined.convert("RGB").save(output_path, "JPEG", quality=90)

alpha_composite を使用することで、通常の paste 関数を使った時のような不快なジャギー(ギザギザ)がなく、ロゴの境界線が滑らかに見えます。

3. SEO最適化のための画像「軽量化」

数MBもある重い画像は、SEOを損なうだけでなく帯域幅も浪費します。私が携わったプロジェクトでは、次の3つのルールを適用することで、ストレージの総容量を200GBから60GB未満に削減できました:

  • Quality 85: これがいわゆる「スイートスポット」です。容量を70%削減しつつ、肉眼ではほとんど違いが分かりません.
  • Optimize=True: 保存前にPillowにカラーパレットを再スキャンさせ、さらに圧縮をかけます。
  • WebPへの変換: この形式は、鮮明さを保ったままJPEGより30〜50%ほど軽量になります。
# WebP形式で高圧縮保存
img.save("optimized.webp", "WEBP", quality=80, method=6)

バッチ処理(一括処理)

実際には、1ファイルずつスクリプトを実行することはありません。私はよく pathlib と組み合わせて、画像フォルダ全体を数秒で一括スキャンします:

from pathlib import Path

def process_all_images(input_dir, output_dir):
    path = Path(input_dir)
    output_path = Path(output_dir)
    output_path.mkdir(exist_ok=True)

    for img_file in path.glob("*.jpg"):
        try:
            with Image.open(img_file) as img:
                # 処理ステップを実行...
                target = output_path / f"done_{img_file.name}"
                img.save(target, quality=85, optimize=True)
                print(f"完了: {img_file.name}")
        except Exception as e:
            print(f"エラー ファイル {img_file}: {e}")

避けるべき「落とし穴」

このツールをサーバーで半年間運用して得た、3つの教訓を紹介します:

  1. 画像の回転エラー (EXIF): iPhoneで撮影した画像などは、Pillowで開くと横向きに回転してしまうことがあります。ImageOps.exif_transpose を使って即座に修正しましょう。
  2. RAM使用量に注意: 10,000pxを超えるような巨大な画像を処理すると、PillowはRAMを大量に消費します。作業が終わったらすぐにファイルを閉じるよう、常に with 文を使用してください。
  3. カラーモード変換の忘れ: スクリプトがエラーで停止するのを防ぐため、透過画像(RGBA)を直接JPEG形式で保存しないでください。JPEG保存の前には必ず convert("RGB") を行いましょう。

結論として、画像処理の自動化は決して難しくありません。数行のPythonコードとPillowライブラリがあれば、外部のSaaSサービスに高額な料金を支払うことなく、独自のプロフェッショナルなシステムを構築できます。

Share: