スクリプト・ファイル自動化

Pillow で元画像を保持したままフォルダ内の画像を一括リサイズする

複数の画像を引き伸ばしたり元ファイルを置き換えたりせず、新しいフォルダへリサイズして保存します。小さな合成画像セットを使うため、想定される出力寸法を手作業でも簡単に計算できます。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者各画像の縦横比を維持し、元画像を変更せずに、多数の画像の小さいコピーを作成したい人向けのガイドです。

準備するもの
  • Python 3.12 と、そのバージョンを起動できるターミナルコマンド。
  • python -m pip install Pillow で Pillow をインストール済みであること。
  • スクリプトが outputs の下に新しいフォルダを作成できる作業フォルダ。
  • 元の合成画像とリサイズ後のコピーの両方を保存できる十分なディスク容量。

01先にリサイズ規則を定義する

目的は、縦横比を維持したまま、すべての画像を 300 by 300 pixel の範囲内に収めることです。すでにその範囲より小さい画像は拡大しません。元画像は変更せず、リサイズした各コピーは別の出力フォルダに保存します。

幅 W、高さ H の画像では、scale を 300/W, 300/H, 1 のうち最も小さい値として計算します。その1つの scale factor から新しい幅と高さを計算します。両方の寸法に同じ係数を使うことで、画像の引き伸ばしを防ぎます。

text
scale = min(300 / width, 300 / height, 1)
new_width = int(width * scale)
new_height = int(height * scale)

02合成画像フォルダを作成する

以下のファイルは合成データであり、この記事のために作成したものです。セットアップスクリプトは、寸法が既知のシンプルな PNG 画像を4枚作成します。画像の見た目そのものは重要ではなく、想定される各リサイズ結果を手作業で確認できるよう寸法を設定しています。

python
from pathlib import Path
from PIL import Image

SOURCE = Path("outputs") / "image_resize_demo"
IMAGES = {
    "landscape.png": ((800, 600), (220, 80, 80)),
    "portrait.png": ((600, 800), (80, 120, 220)),
    "small.png": ((200, 100), (80, 180, 100)),
    "wide.png": ((1200, 300), (180, 120, 60)),
}

SOURCE.parent.mkdir(parents=True, exist_ok=True)
SOURCE.mkdir()  # Stop if this synthetic source already exists.

for filename, (size, color) in IMAGES.items():
    image = Image.new("RGB", size, color)
    target = SOURCE / filename
    image.save(target, format="PNG")
text
python create_resize_demo.py
ファイル元の幅元の高さ縦横比
landscape.png8006004:3
portrait.png6008003:4
small.png2001002:1
wide.png12003004:1

4枚の元画像には合計 1,240,000 pixels があります: 480,000 + 480,000 + 20,000 + 360,000。pixel 数はこの例を理解するのに役立ちますが、PNG 圧縮は画像内容に依存するため、bytes 単位のファイルサイズとは同じではありません。

03想定される4つの寸法を手作業で計算する

landscape.png では制限となる寸法は幅です: 300/800 = 0.375。両方の寸法に 0.375 を掛けると 300 by 225 になります。portrait.png では高さが制限になります: 300/800 = 0.375 なので、225 by 300 になります。

small.png は両方の寸法がすでに 300 以下なので、そのまま収まります。scale は最大 1 に制限されるため、200 by 100 のままです。wide.png では幅が制限になります: 300/1200 = 0.25 なので、300 by 75 になります。

ファイルScale想定出力出力 pixels
landscape.png0.375300 × 22567,500
portrait.png0.375225 × 30067,500
small.png1200 × 10020,000
wide.png0.25300 × 7522,500

したがって、リサイズしたコピーに含まれる pixel 数の合計は 177,500 です。元画像は別に残るため、この値はディスク容量の削減量を示すものではありません。

04フォルダ内の画像を新しい保存先へリサイズする

次のスクリプトを batch_image_resize.py として保存してください。SOURCE 直下にある PNG, JPEG, WebP ファイルを処理します。サブフォルダは再帰的に検索しません。保存先は exist_ok を付けずに mkdir で作成するため、出力ディレクトリがすでに存在する場合は、以前の結果と新しい結果を混在させずにスクリプトが停止します。

python
from pathlib import Path
from PIL import Image, ImageOps

SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"
MAX_WIDTH = 300
MAX_HEIGHT = 300
SUPPORTED = {".png", ".jpg", ".jpeg", ".webp"}


def target_size(width: int, height: int) -> tuple[int, int]:
    scale = min(MAX_WIDTH / width, MAX_HEIGHT / height, 1)
    new_width = max(1, int(width * scale))
    new_height = max(1, int(height * scale))
    return new_width, new_height


def main() -> None:
    if not SOURCE.is_dir():
        raise FileNotFoundError(f"Source folder not found: {SOURCE}")
    if MAX_WIDTH < 1 or MAX_HEIGHT < 1:
        raise ValueError("Maximum dimensions must be positive integers.")

    source_resolved = SOURCE.resolve()
    output_resolved = OUTPUT_DIR.resolve()
    if source_resolved == output_resolved:
        raise ValueError("SOURCE and OUTPUT_DIR must be different.")

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()  # Refuse to reuse an existing destination.

    candidates = sorted(
        path for path in SOURCE.iterdir()
        if path.is_file() and path.suffix.lower() in SUPPORTED
    )

    resized_count = 0
    unchanged_size_count = 0

    for source_path in candidates:
        output_path = OUTPUT_DIR / source_path.name
        if output_path.exists():
            raise FileExistsError(f"Output already exists: {output_path}")

        with Image.open(source_path) as opened:
            image = ImageOps.exif_transpose(opened)
            image.load()
            old_size = image.size
            new_size = target_size(*old_size)

            if new_size == old_size:
                result = image.copy()
                unchanged_size_count += 1
            else:
                result = image.resize(new_size, Image.Resampling.LANCZOS)
                resized_count += 1

            save_options = {}
            if source_path.suffix.lower() in {".jpg", ".jpeg"}:
                save_options["quality"] = 90
            result.save(output_path, **save_options)
            result.close()

        print(f"{source_path.name}: {old_size[0]}x{old_size[1]} -> {new_size[0]}x{new_size[1]}")

    print(f"Processed {len(candidates)} images.")
    print(f"Resized: {resized_count}; already within limit: {unchanged_size_count}.")
    print(f"Output folder: {OUTPUT_DIR.as_posix()}")


if __name__ == "__main__":
    main()

ImageOps.exif_transpose は、EXIF orientation 情報が存在する場合、リサイズ計算の前に画像の向きを適用します。合成 PNG ファイルにはこの向き情報がないため、表示寸法は上記の寸法のままです。

05想定される出力を比較する

ファイル名はソート順で処理されるため、想定されるコンソール行は landscape.png, portrait.png, small.png, wide.png の順です。3枚の画像は小さくなり、1枚は元の寸法のままです。

text
landscape.png: 800x600 -> 300x225
portrait.png: 600x800 -> 225x300
small.png: 200x100 -> 200x100
wide.png: 1200x300 -> 300x75
Processed 4 images.
Resized: 3; already within limit: 1.
Output folder: outputs/image_resize_result

この想定出力は実行ログを取得したものではなく、手作業で導出したものです。重要な確認項目は寸法、ファイル数、元画像が outputs/image_resize_demo に残っていることです。

確認項目想定値
元フォルダ内のファイル数4
出力フォルダ内のファイル数4
リサイズされたファイル3
すでに制限内だったファイル1
最大出力幅300
最大出力高さ300

06生成された画像を自分で検証する

ファイル名やコンソール表示だけに頼らないでください。生成されたファイルを開いて寸法を確認します。以下の任意のチェッカーは両方のフォルダを読み取り、寸法を報告します。書き込み処理は行いません。

python
from pathlib import Path
from PIL import Image

SOURCE = Path("outputs") / "image_resize_demo"
OUTPUT_DIR = Path("outputs") / "image_resize_result"

for source_path in sorted(SOURCE.glob("*.png")):
    output_path = OUTPUT_DIR / source_path.name
    with Image.open(source_path) as original, Image.open(output_path) as resized:
        print(
            f"{source_path.name}: "
            f"original={original.size}, output={resized.size}"
        )
  • すべての出力幅が 300 以下で、すべての出力高さが 300 以下であることを確認します。
  • landscape.png が 4:3、portrait.png が 3:4、wide.png が 4:1 のままであることを確認します。
  • small.png が 300 by 150 に拡大されず、200 by 100 のままであることを確認します。
  • 元ファイル自体が書き換えられていないことを追加で確認したい場合は、更新日時またはハッシュを比較します。
  • OUTPUT_DIR を変更せずにリサイズスクリプトを2回目に実行します。別のバッチを処理する前に FileExistsError で停止するはずです。

07よくあるエラーを確認する

症状確認すること
ModuleNotFoundError: No module named PILpython -m pip install Pillow を使い、スクリプトを実行するのと同じ Python 環境に Pillow をインストールします。
FileNotFoundErrorSOURCE とターミナルの現在の作業ディレクトリを確認します。この例では、先に合成セットアップスクリプトを実行してください。
FileExistsError出力フォルダがすでに存在します。自動上書きを許可せず、内容を確認して新しい保存先を選択します。
Image cannot be identified拡張子は対応形式に見えても、内容が破損しているか、実際には画像ではない可能性があります。失敗したバッチは未完了として扱います。
Unexpected rotationorientation metadata と pixel orientation が異なる場合があります。リサイズ前に ImageOps.exif_transpose を使用していますが、重要な画像は目視でも確認してください。
Unexpected output file size in bytespixel 寸法だけでは圧縮後のファイルサイズは決まりません。形式、画像内容、メタデータ、JPEG quality も影響します。

一部のファイルを書き込んだ後に処理が失敗しても、スクリプトはその部分的な結果を削除しません。調査のために保存先を残すか、問題を修正した後で新しい出力ディレクトリを使用してください。

08実際の画像に使う前に制限を理解する

この例では、1つのフォルダ直下にあるファイルだけを扱い、PNG, JPEG, WebP 拡張子をサポートします。ネストされたディレクトリ構造の再現、アニメーションフレームごとのリサイズ、すべての形式のメタデータ保持、カラーマネジメント情報が完全に同じ状態で残ることは保証しません。

リサンプリングは pixel データを変更します。画像の寸法が同じままでも、開いて保存することでエンコード済み bytes やメタデータが変化する場合があります。JPEG 出力は非可逆であり、このスクリプトでは JPEG コピーを quality 90 で保存します。元の JPEG bitstream や元の圧縮設定を保持するものではありません。

int 計算では小数寸法を切り捨てます。そのため任意の元サイズでは、画像寸法を整数にする必要があることから、結果の pixel 比率が数学的な比率とわずかに異なる場合があります。このガイドの合成寸法は割り切れるため、4つの想定結果に曖昧な丸めはありません。

実行・検証の記録

2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · Pillow 必須 · 未実行

  • 4枚の合成元画像の寸法 800x600, 600x800, 200x100, 1200x300 を手作業で確認しました。
  • scale factor 0.375, 0.375, 1, 0.25 を手作業で計算しました。
  • 想定出力寸法 300x225, 225x300, 200x100, 300x75 を手作業で導出しました。
  • 元画像の pixel 合計を 1,240,000、出力画像の pixel 合計を 177,500 と手作業で計算しました。
  • 元ファイルが読み取り用に開かれ、結果が別フォルダに保存され、保存先フォルダがすでに存在する場合に実行が停止することをコードで確認しました。
  • 想定されるコンソール出力と件数を手作業で導出しました。
検証範囲の限界
  • この回答の作成者はコードを実行しておらず、Pillow の動作やファイルシステムへの書き込みはここではテストしていません。
  • JPEG, WebP, EXIF orientation, 破損ファイル, color profiles, animation, metadata preservation については説明しましたが、テストしていません。
  • 圧縮後の bytes 単位のファイルサイズは、画像内容とエンコードに依存するため予測していません。
  • 公式ドキュメントの URL は既知のドキュメント所在地に基づいて記載していますが、ライブでは確認していません。

サイト全体の執筆・検証方針

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。