Pythonで画像を一括リサイズ:Pillowでフォルダ内の画像をまとめて縮小(Windows/Mac対応)

「Pythonで画像を一括リサイズ:Pillowでフォルダ内の画像をまとめて縮小(Windows/Mac対応)」のアイキャッチ
  • URLをコピーしました!

フォルダに画像を入れてコマンドを1回実行。
Python(Pillow)でまとめてリサイズし、EXIF(写真の向き)補正、拡大しない設定、品質の調整、WebP出力まで対応します。

Windows/Macでそのまま動き、共有・メール添付・クラウド容量の節約・資料作成・EC出品など幅広い用途で使えます。

目次

まずはこれだけ(最短コース)

pipや仮想環境が不安な方はこの1冊

1) フォルダを用意

  • デスクトップなど好きな場所に img-work フォルダを作る
  • その中に input_images フォルダを作り、小さくしたい写真を全部ここに入れる

2) 準備(Pillowという道具を入れる)

Mac:

python3 -m pip install --upgrade pip
python3 -m pip install Pillow

Windows(PowerShell):

py -3 -m pip install --upgrade pip
py -3 -m pip install Pillow

3) 下のスクリプトをメモ帳にコピペして保存

  • ファイル名:resize_simple.py
  • 保存場所:img-work フォルダの中(input_images と同じ階層)
from pathlib import Path
from PIL import Image, ImageOps

# --- HEICを入れていれば使う(任意) ---
try:
    import pillow_heif
    pillow_heif.register_heif_opener()
    HEIF_ENABLED = True
except ImportError:
    HEIF_ENABLED = False
# ----------------------------------------


# ===== 設定(ここだけ変えればOK) =====
INPUT_DIR = Path("./input_images")   # 入力フォルダ
OUTPUT_DIR = Path("./resized")       # 出力フォルダ
LONG_EDGE = 1600                       # 長辺のピクセル数(例: 1600)
OUTPUT_FORMAT = "jpg"                 # "keep"=そのまま / "jpg" / "webp" / "png"
QUALITY = 85                           # 画像のきれいさ(数が大きいほどきれい&重い)
# ======================================

EXTS = {".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"}
if HEIF_ENABLED:
    EXTS |= {".heic", ".heif"}

def calc_new_size(w, h, long_edge=LONG_EDGE):
    scale = long_edge / max(w, h)
    scale = min(scale, 1.0)  # 小さい画像は拡大しない
    return max(1, int(w*scale)), max(1, int(h*scale))

def ensure_rgb_for_jpeg(img: Image.Image) -> Image.Image:
    # 透過PNGをJPEGにするときは白背景にしておく
    if img.mode in ("RGBA", "LA"):
        bg = Image.new("RGB", img.size, (255, 255, 255))
        bg.paste(img, mask=img.split()[-1])
        return bg
    if img.mode in ("P", "CMYK"):
        return img.convert("RGB")
    return img

def main():
    files = [p for p in INPUT_DIR.rglob("*") if p.suffix.lower() in EXTS]
    if not files:
        print("画像が見つかりません。INPUT_DIRを確認してください。")
        return

    for src in files:
        rel = src.relative_to(INPUT_DIR)
        out_ext = src.suffix.lower() if OUTPUT_FORMAT == "keep" else "." + OUTPUT_FORMAT
        out = (OUTPUT_DIR / rel).with_suffix(out_ext)
        out.parent.mkdir(parents=True, exist_ok=True)

        try:
            with Image.open(src) as im:
                im = ImageOps.exif_transpose(im)  # 写真の向きを自動で直す
                w, h = im.size
                nw, nh = calc_new_size(w, h)
                if (nw, nh) != (w, h):
                    im = im.resize((nw, nh), Image.Resampling.LANCZOS)

                exif = im.info.get("exif")
                icc  = im.info.get("icc_profile")

                fmt = (OUTPUT_FORMAT if OUTPUT_FORMAT != "keep" else src.suffix[1:]).upper()
                if fmt == "JPG":
                    fmt = "JPEG"
                if fmt in ("TIF", "TIFF"):
                    fmt = "TIFF"
                if fmt in ("HEIC", "HEIF"):
                    fmt = "HEIF"

                save_kwargs = {}
                if fmt in ("JPEG", "WEBP"):
                    save_kwargs["quality"] = QUALITY
                if fmt == "JPEG":
                    im = ensure_rgb_for_jpeg(im)
                    save_kwargs.update(dict(optimize=True, progressive=True))
                    if exif: save_kwargs["exif"] = exif
                    if icc:  save_kwargs["icc_profile"] = icc
                elif fmt == "PNG":
                    if icc: save_kwargs["icc_profile"] = icc
                    save_kwargs["optimize"] = True
                elif fmt == "WEBP":
                    save_kwargs.update(dict(method=6))
                    if icc: save_kwargs["icc_profile"] = icc

                im.save(out, format=fmt, **save_kwargs)
                print(f"OK: {rel} -> {out.relative_to(OUTPUT_DIR)} ({w}x{h}→{nw}x{nh})")
        except Exception as e:
            print(f"ERROR: {src.name} / {e}")

    print(f"完了: 出力先 {OUTPUT_DIR.resolve()}")

if __name__ == "__main__":
    main()

4) 実行する

Mac:

cd /あなたの/img-work の場所
python3 resize_simple.py

Windows(PowerShell):

cd "C:\Users\あなたの名前\Desktop\img-work"
py -3 resize_simple.py

終わったら img-work の中に resized フォルダができ、そこに小さくなった写真が入ります。

迷ったら:何も変えずに LONG_EDGE=1600 のまま実行でOK。ウェブ共有や資料作成でちょうど良いサイズ感です。

要点(最初に)

  • 目的:指定フォルダ内の画像を、長辺○○pxまたは幅×高さの枠内一括リサイズ。アスペクト比は維持。
  • 対応:JPEG/PNG/WebP/TIFF。EXIF(写真の向きの情報)の回転補正&(可能なら)ICCプロファイル(色の情報)を保持。
  • 安全設計:デフォルトは拡大しない(画質劣化を防ぐ)。
  • 追加機能:出力フォーマットの強制変換/WebP同時出力/品質指定。

この記事で作るもの

  • コピペで使えるPythonスクリプトresize_images.py
  • Windows/Macのインストール~実行コマンド
  • 実務TIPS(WordPress/SWELLでの使いどころ、OGP・サムネの目安、失敗しがちなポイント)
  • 目的別レシピ(WebP化、正方形サムネ、目標KBに落とす など)

動作環境と準備

  • Python 3.9 以上推奨(3.8でも可)
  • 追加ライブラリ:Pillow

1) Pythonの確認

Mac

python3 --version

Windows(PowerShell)

py -3 --version

2) プロジェクト用フォルダ&仮想環境(任意)

mkdir img-resizer && cd img-resizer
python3 -m venv .venv           # Windowsは: py -3 -m venv .venv
source .venv/bin/activate       # Windowsは: .venv\Scripts\activate

3) Pillowのインストール

pip install --upgrade pip
pip install Pillow

3.5) iPhoneのHEICを扱う(任意)

HEIC/HEIF(iPhoneの標準写真)も読みたい場合は追加します。

Mac/Windows 共通

pip install pillow-heif

上記のコマンドは一度入れればOKです。以降のサンプルコードでインポート+有効化しています。

補足--format keep のとき、HEICは内部的に HEIFフォーマットとして保存されます(拡張子は .heic でもOK)。エクスプローラ/プレビューで開けない場合は、--format jpg などに変換して出力してください。

iPhone写真をPCへ高速転送。

HEICのまま取り込んで、本記事の手順で一括変換できます。

コピペで使える一括リサイズスクリプト

ファイル名resize_images.py

役割

フォルダ内を再帰的に走査し、長辺基準または枠内(最大幅×最大高)で一括リサイズ。
既定では拡大しない。EXIF(写真の向きの情報)の回転補正、ICCプロファイル(色の情報)の引き継ぎを試み、JPEG/WebP品質を指定可能。WebP同時出力にも対応。

import argparse
from pathlib import Path
from PIL import Image, ImageOps

# --- HEICを入れていれば使う(任意) ---
try:
    import pillow_heif
    pillow_heif.register_heif_opener()
    HEIF_ENABLED = True
except ImportError:
    HEIF_ENABLED = False
# ----------------------------------------


EXTS = {".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"}
if HEIF_ENABLED:
    EXTS |= {".heic", ".heif"}


def calc_new_size(w, h, *, long_edge=None, max_w=None, max_h=None, no_upscale=True):
    if long_edge:
        scale = long_edge / max(w, h)
    else:
        max_w = max_w or 10**9
        max_h = max_h or 10**9
        scale = min(max_w / w, max_h / h)
    if no_upscale:
        scale = min(scale, 1.0)
    return (max(1, int(w * scale)), max(1, int(h * scale)))


def ensure_rgb_for_jpeg(img: Image.Image) -> Image.Image:
    if img.mode in ("RGBA", "LA"):
        bg = Image.new("RGB", img.size, (255, 255, 255))
        bg.paste(img, mask=img.split()[-1])
        return bg
    if img.mode in ("P", "CMYK"):
        return img.convert("RGB")
    return img


def save_image(im: Image.Image, out_path: Path, fmt: str, quality: int, exif, icc):
    fmt = fmt.upper()
    if fmt == "JPG":
        fmt = "JPEG"
    if fmt in ("TIF", "TIFF"):
        fmt = "TIFF"
    if fmt in ("HEIC", "HEIF"):
        fmt = "HEIF"

    save_kwargs = {}
    if fmt in ("JPEG", "WEBP"):
        save_kwargs["quality"] = quality
    if fmt == "JPEG":
        im = ensure_rgb_for_jpeg(im)
        save_kwargs.update(dict(optimize=True, progressive=True))
        if exif: save_kwargs["exif"] = exif
        if icc:  save_kwargs["icc_profile"] = icc
    elif fmt == "PNG":
        if icc: save_kwargs["icc_profile"] = icc
        save_kwargs["optimize"] = True
    elif fmt == "WEBP":
        save_kwargs.update(dict(method=6))  # 最高圧縮(遅い方)
        if icc: save_kwargs["icc_profile"] = icc

    im.save(out_path, format=fmt, **save_kwargs)


def process_file(src_path: Path, dst_root: Path, args):
    rel = src_path.relative_to(args.src)
    out_ext = ("." + args.format.lower()) if args.format else src_path.suffix.lower()
    out_path = (dst_root / rel).with_suffix(out_ext)
    out_path.parent.mkdir(parents=True, exist_ok=True)

    try:
        with Image.open(src_path) as im:
            im = ImageOps.exif_transpose(im)  # EXIF(写真の向きの情報)回転補正
            orig_w, orig_h = im.size

            new_w, new_h = calc_new_size(
                orig_w, orig_h,
                long_edge=args.long_edge,
                max_w=args.max_width,
                max_h=args.max_height,
                no_upscale=not args.allow_upscale
            )

            if (new_w, new_h) != (orig_w, orig_h):
                im = im.resize((new_w, new_h), Image.Resampling.LANCZOS)

            exif = im.info.get("exif")
            icc  = im.info.get("icc_profile")

            fmt_hint = None if (args.format in (None, "keep")) else args.format
            primary_fmt = (fmt_hint or src_path.suffix[1:]).upper()
            save_image(im, out_path, primary_fmt, args.quality, exif, icc)

            if args.also_webp:
                webp_path = out_path.with_suffix(".webp")
                save_image(im, webp_path, "WEBP", args.webp_quality or args.quality, exif, icc)

            return True, f"{src_path.name}: {orig_w}x{orig_h} -> {new_w}x{new_h}"
    except Exception as e:
        return False, f"{src_path.name}: ERROR {e}"


def main():
    p = argparse.ArgumentParser(description="画像の一括リサイズ")
    p.add_argument("--src", type=Path, required=True, help="入力フォルダ")
    p.add_argument("--dst", type=Path, default=Path("./resized"), help="出力フォルダ")

    g = p.add_mutually_exclusive_group(required=True)
    g.add_argument("--long-edge", type=int, help="長辺のピクセル数(例: 1600)")
    g.add_argument("--max-width", type=int, help="最大幅(例: 1600)")
    p.add_argument("--max-height", type=int, help="最大高(例: 1200)")

    p.add_argument("--quality", type=int, default=85, help="JPEG/WEBP品質(1-95推奨)")
    p.add_argument("--format", choices=["keep","jpg","jpeg","png","webp"], help="出力フォーマットを強制変換")
    p.add_argument("--allow-upscale", action="store_true", help="小さな画像も拡大してよい")

    p.add_argument("--also-webp", action="store_true", help="主出力に加えてWebPも同時保存")
    p.add_argument("--webp-quality", type=int, help="WebPの品質(未指定なら --quality と同じ)")

    args = p.parse_args()

    files = [p for p in args.src.rglob("*") if p.suffix.lower() in EXTS]
    if not files:
        print("対象画像が見つかりませんでした。")
        return

    print(f"Found {len(files)} files.")
    ok = err = 0
    for f in files:
        success, msg = process_file(f, args.dst, args)
        print(msg)
        ok += int(success)
        err += int(not success)
    print(f"Done. success={ok}, error={err}, out={args.dst.resolve()}")


if __name__ == "__main__":
    main()
ポイント
  • ImageOps.exif_transpose() でスマホ写真の縦横回転問題を自動補正。
  • デフォルトで拡大禁止(小さい元画像はそのまま)。--allow-upscale を付けたときだけ拡大。
  • JPEG/WebPは --quality で品質指定(85前後が現実解)。
  • --also-webpJPEGなどの主出力+WebP同時保存に対応。

使い方(実行コマンド例)

1) 長辺を 1600px にそろえる(拡大禁止、品質85)

python3 resize_images.py --src ./input_images --dst ./resized --long-edge 1600

2) 1600×1200 の枠内に収める(アスペクト比は保持)

python3 resize_images.py --src ./input_images --dst ./resized --max-width 1600 --max-height 1200

3) 強制的に WebP 化したい

python3 resize_images.py --src ./input_images --dst ./webp --long-edge 1600 --format webp --quality 80

4) JPEGを主出力にしつつ、WebPも同時に保存

python3 resize_images.py --src ./input_images --dst ./out --long-edge 1600 --format jpg --quality 85 --also-webp --webp-quality 80

5) 小さい画像も拡大していい場合

python3 resize_images.py --src ./input_images --dst ./large --long-edge 2048 --allow-upscale
Windowsの場合

python3 の代わりに py -3 でもOK。

注意--src(入力)と --dst(出力)は別フォルダにしてください。出力先を入力と同じにすると、処理済み画像を再スキャンしてしまう場合があります。

用途別サイズ目安(Web/共有/印刷)

  • Webページ・資料共有:長辺 1600〜2048px
  • SNS/OGP:1200×630
  • 正方形サムネ:1080×1080
  • メール添付を軽く:長辺 1280〜1600px
  • 印刷(L判〜A4):長辺 3000px前後 / JPEG品質90〜

目的別レシピ(小スクリプト)

A. 正方形サムネを作る(中央クロップ)

from PIL import Image, ImageOps
# --- HEICを入れていれば使う(任意) ---
try:
    import pillow_heif
    pillow_heif.register_heif_opener()
    HEIF_ENABLED = True
except ImportError:
    HEIF_ENABLED = False
# ----------------------------------------

def square_crop_resize(src, dst, size=1080):
    with Image.open(src) as im:
        im = ImageOps.exif_transpose(im)
        im = ImageOps.fit(im, (size, size), method=Image.Resampling.LANCZOS, centering=(0.5, 0.5))
        im.save(dst, quality=90, optimize=True, progressive=True)

B. 目標ファイルサイズ(KB)に近づける(JPEGの品質調整)

from PIL import Image, ImageOps
import io

# --- HEICを入れていれば使う(任意) ---
try:
    import pillow_heif
    pillow_heif.register_heif_opener()
    HEIF_ENABLED = True
except ImportError:
    HEIF_ENABLED = False
# ----------------------------------------

def save_under_kb(src, dst, max_kb=300, quality_start=90):
    with Image.open(src) as im:
        im = ImageOps.exif_transpose(im).convert("RGB")
        q = quality_start
        while q >= 50:
            buf = io.BytesIO()
            im.save(buf, format="JPEG", quality=q, optimize=True, progressive=True)
            if len(buf.getvalue()) <= max_kb * 1024:
                with open(dst, "wb") as f:
                    f.write(buf.getvalue())
                return q
            q -= 5
    return None

よくある質問・トラブルシュート

zsh: command not found: python3(Mac)

Homebrew等でPythonを入れるか、python --version で既存のPythonに切り替えて実行してください。pyenv を使ってもOK。

ModuleNotFoundError: No module named 'PIL'

pip install Pillow を忘れている可能性。仮想環境がアクティブかも確認。

画像が横向き/逆さになる

スクリプト内で ImageOps.exif_transpose() を使っています。自前処理の際はこの一行を必ず入れてください。

PNGの透過が白くなる

JPEGは透過不可。透過を保ちたいときはPNG/WEBPで保存(--format png or --format webp)。

色が違って見える

ICCプロファイル(色の情報)を付与していますが、ブラウザ/アプリ差で見え方が変わる場合があります。sRGBで一元化したい場合は img = img.convert('RGB') の上でICCなし保存も検討。

既存ファイルを上書きしたい

安全のため別フォルダ出力にしています。上書きは事故の元なのでおすすめしません(Git管理やバックアップがあれば対応可)。

実務TIPS

  • 品質(quality):85は視覚品質とサイズの妥協点。写真多めの記事は80〜85、図版・文字多めは90でもOK。
  • WebP:ほぼ全ブラウザ対応。JPEG + WebP併用にしておくと、テーマやCDNの最適化にも噛みやすい。
  • 命名規則post-slug_001.jpg のように連番&記事スラッグを入れると、後で管理が楽。(自動リネームは別スクリプトで)
  • CDN/最適化:Cloudflare Images / 画像圧縮プラグインの前処理として、このスクリプトで事前縮小しておくと負荷が減ります。

撮影→圧縮→アップが一気に短縮。

元データは外付けに退避→記事用は本スクリプトで軽量化

まとめ

  • Pillowだけで現実運用に耐える一括リサイズが可能。
  • まずは 長辺1600px・品質85 から。必要に応じて WebP同時出力正方形サムネを併用。
  • WordPress/SWELLでも、アップ前に最適サイズへ整えると表示が軽く、SEOにもプラス。
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

当ブログの運営者であり全ての記事を書いた陰の者。
なんでも好きなものだけ書きます。
X(旧Twitter)でも更新情報とたまにくだらないことを発信中。
コメント解放しておりませんので、Xの方で感想いただけると嬉しくて更新が早くなる傾向にあります。調子乗りです。

目次