Python Pillowで1000枚の画像を一括圧縮:完全ガイド

ぜ画像を一括圧縮するのか

カメラから書き出した1000枚の高解像度写真があり、それぞれ約8MB、ディレクトリすべて体でほぼ8GBを消費しているとします。Webサイトへのアップロード、プレゼンへの埋め込み、メール送信が目のであれば、その容量は完全に容認できいものです。グラフィックエディタで1枚ずつ圧縮するのは非効率でミスも起きやすい、まさにここでスクリプトによる一括処理が威力を発揮します。

PythonのPillowライブラリは画像処理のデファクトスタンダードです。JPEG、PNG、WebP、BMP、TIFFどの主フォーマットの読み書きをサポートし、品質ファクタ、サンプリングレート、カラーモードをきめ細かく制御できます。数十行のPythonで、ディレクトリツリーすべて体を走査し、統一された圧縮戦略を適用し、出力で元のフォルダ構造を維持できます。

本記事ではPillowの基本的なのなのなの使い方から始め、段階的に完全に実行できるマルチスレッド対応の一括圧縮スクリプトを構築します。また、EXIF保持、フォーマット変換、インテリジェントパラメータ選択などの実用のエンジニアリングの詳細も扱います。


Pillowのインストールと基本的なのなのなの使い方

インストール

PillowはPIL(Python Imaging Library)の活発にメンテナンスされているフォークですが、インポート名はPILのままです。pipでインストールします:


pip install Pillow

大量の画像を処理する必要がある場合や、より高速デコードが必要場合は、プラグインサポートを含む完全版をインストールします:


pip install "Pillow[all]"

インストール後、バージョンを確認します:


from PIL import Image, __version__
print(__version__)  # 例: 10.4.0

基本的なのなのな操作

Pillowの中心のエントリポイントはImageクラスです。最も一般的3つの操作は、開く、変更、保存です:


from PIL import Image

# 画像を開く(遅延読み込み、ピクセルデータは初回アクセス時に読み込まれる)
img = Image.open("photo.jpg")
print(img.format, img.size, img.mode)  # JPEG (4000, 3000) RGB

# ピクセル値を取得
pixel = img.getpixel((100, 100))

# リサイズ
resized = img.resize((1920, 1080))

# 保存
resized.save("photo_small.jpg", quality=85)

いくつか留意すべき点があります:Image.open()が返すオブジェクトはファイル参照を保持し、明示的に閉じるかwithブロックを抜けるまで解放されません。一括処理では、必ずコンテキストマネージャを使用するか、明示的にclose()を呼び出してください。そうしいとファイルハンドルがリークします。


単一画像の圧縮例

一括圧縮の基礎は単一画像の処理です。まず、JPEG品質調整、解像度の縮小、フォーマット変換をサポートする関数を実装します。

JPEG品質の調整

JPEGのqualityパラメータは1から100の範囲で、量子化テーブルのスケーリングを直接制御します。ファイファイルサイズと画質のバランスを取るための主つまみです:


from PIL import Image

def compress_jpeg(input_path, output_path, quality=75):
    """指定した品質ファクタでJPEGを圧縮"""
    with Image.open(input_path) as img:
        # JPEGは透過をサポートしい。RGBA/Pモードは先に変換が必要
        if img.mode in ("RGBA", "P", "LA"):
            img = img.convert("RGB")
        elif img.mode != "RGB":
            img = img.convert("RGB")
        img.save(
            output_path,
            format="JPEG",
            quality=quality,
            optimize=True,       # Huffmanエンコーディングの最適化を有効化
            progressive=True,    # プログレッシブJPEGを生成。わずかに大きいが読み込み体験が良い
        )

# 異る品質ファクタをで比較
compress_jpeg("photo.jpg", "q95.jpg", quality=95)
compress_jpeg("photo.jpg", "q75.jpg", quality=75)
compress_jpeg("photo.jpg", "q50.jpg", quality=50)

optimize=Trueにより、Pillowは保存時にもう一度データをスキャンし、最適Huffmanエンコーディングテーブルを再度構築します。これにより通常、保存がわずかに遅くる代わりにファイファイルサイズがさらに2-5%削減されます。

解像度の調整

Webテーブル示の場合、4000x3000のオリジナルは必要よりはるかに大きいです。thumbnail()を使ってより率のに縮小するのが最も効果のアプローチです。解像度を半分にするとファイファイルサイズは約75%削減されます:


from PIL import Image

def resize_image(input_path, output_path, max_size=1920, quality=80):
    """アスペクトよりを維持したまま最長辺をmax_sizeにスケール"""
    with Image.open(input_path) as img:
        # thumbnailはその場で変更し、指定した寸法を超えることはい
        # Image.Resampling.LANCZOSは最高品質のダウンサンプリングアルゴリズム
        img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
        if img.mode != "RGB":
            img = img.convert("RGB")
        img.save(output_path, format="JPEG", quality=quality, optimize=True)

resize_image("photo.jpg", "photo_1920.jpg", max_size=1920, quality=80)

thumbnail()resize()の違いは、thumbnail()はアスペクトよりを維持し、画像を拡大しいことです。最も終の寸法は常に指定値以下にります。resize()は画像を正確ターゲット寸法に強制します。一括処理では、ほぼ常にthumbnail()を使います。

フォーマット変換

PNGをWebPに変換するのは一般の最適化です。WebPはPNGより26%小さく、と等品質のJPEGより25-35%小さく、透過もサポートします:


from PIL import Image

def convert_to_webp(input_path, output_path, quality=80, lossless=False):
    """任意のフォーマットをWebPに変換"""
    with Image.open(input_path) as img:
        img.save(
            output_path,
            format="WebP",
            quality=quality,
            lossless=lossless,   # Trueの場合、ロスレスモードを使用
            method=6,            # 圧縮努力 0-6。6が最も遅いが最も小
        )

convert_to_webp("icon.png", "icon.webp", quality=85)
convert_to_webp("logo.png", "logo_lossless.webp", lossless=True)

methodパラメータは圧縮努力を0から6で制御します。値が高いほど圧縮率はへ上しますが時間がかかります。最も終のファイファイルサイズが重要一括処理では6に設定し、速度重視ら4が良いバランスです。


完全一括圧縮スクリプト

ここで、単一画像のロジックを完全一括スクリプトに組み立てます。このスクリプトは入力ディレクトリ内のすべての画像を走査し、出力で元のフォルダ構造を維持し、処理の進行状況とすべて体の圧縮結果をテーブル示します。

以下のフローチャートはすべて体の一括処理のワークフローを示しています:

Python batch image compression processing flowchart: directory traversal, PIL/Pillow processing, quality adjustment, output

完全スクリプト:


"""
batch_compress.py - 画像一括圧縮スクリプト
使用メソッド: python batch_compress.py <input_dir> <output_dir> [--quality 75] [--max-size 1920]
"""
import os
import sys
import argparse
from pathlib import Path
from PIL import Image

SUPPORTED_FORMATS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff", ".tif"}


def compress_single(input_path, output_path, quality=75, max_size=1920,
                    target_format=None, keep_exif=True):
    """単一画像を圧縮"""
    with Image.open(input_path) as img:
        # 1. EXIFデータを抽出(撮影時刻、カメラモデル、GPSど)
        exif_data = img.info.get("exif", b"") if keep_exif else b""

        # 2. カラーモード変換: JPEGは透過をサポートしい
        out_format = (target_format or img.format or "JPEG").upper()
        if out_format in ("JPEG", "JPG") and img.mode in ("RGBA", "P", "LA"):
            # 変換後の黒い領域を避けるため白背景に合成
            background = Image.new("RGB", img.size, (255, 255, 255))
            if img.mode == "P":
                img = img.convert("RGBA")
            background.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None)
            img = background
        elif img.mode not in ("RGB", "RGBA", "L"):
            img = img.convert("RGB")

        # 3. 解像度調整: 縮小のみ、拡大はしい
        if max_size and max(img.size) > max_size:
            img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)

        # 4. 保存
        save_kwargs = {"optimize": True}
        if out_format in ("JPEG", "JPG"):
            save_kwargs["quality"] = quality
            save_kwargs["progressive"] = True
            if exif_data:
                save_kwargs["exif"] = exif_data
        elif out_format == "WEBP":
            save_kwargs["quality"] = quality
            save_kwargs["method"] = 6
        elif out_format == "PNG":
            save_kwargs["optimize"] = True

        img.save(output_path, format=out_format, **save_kwargs)


def batch_compress(input_dir, output_dir, quality=75, max_size=1920,
                   target_format=None, keep_exif=True):
    """ディレクトリすべて体を一括圧縮"""
    input_dir = Path(input_dir)
    output_dir = Path(output_dir)

    # 処理対象のすべてファイルを収集
    tasks = []
    for root, _, files in os.walk(input_dir):
        for filename in files:
            if Path(filename).suffix.lower() in SUPPORTED_FORMATS:
                src = Path(root) / filename
                rel = src.relative_to(input_dir)
                dst = output_dir / rel
                # ターゲットフォーマットに基づいて拡張子を調整
                if target_format:
                    dst = dst.with_suffix(f".{target_format.lower()}")
                tasks.append((src, dst))

    total = len(tasks)
    if total == 0:
        print("サポートされている画像ファイルが見つかりません")
        return

    print(f"{total}枚の画像が見つかりました。圧縮を開始します...\n")

    processed = 0
    total_original = 0
    total_compressed = 0
    errors = []

    for src, dst in tasks:
        # 出力ディレクトリが存ですることを確認
        dst.parent.mkdir(parents=True, exist_ok=True)

        original_size = src.stat().st_size
        try:
            compress_single(src, dst, quality, max_size, target_format, keep_exif)
            new_size = dst.stat().st_size
            total_original += original_size
            total_compressed += new_size
        except Exception as e:
            errors.append((src, str(e)))
            new_size = original_size  # 失敗時は元のサイズとしてカウント

        processed += 1
        pct = processed / total * 100
        saved = (1 - new_size / original_size) * 100 if original_size else 0
        print(f"[{pct:5.1f}%] ({processed}/{total}) {src.relative_to(input_dir)}  "
              f"{original_size // 1024}KB -> {new_size // 1024}KB  (-{saved:.1f}%)")

    # サマリーレポート
    print(f"\n{'=' * 50}")
    print(f"完: {processed}件成功, {len(errors)}件失敗")
    if total_original > 0:
        ratio = total_original / total_compressed if total_compressed else 0
        saved_mb = (total_original - total_compressed) / 1024 / 1024
        print(f"元のサイズ: {total_original / 1024 / 1024:.2f} MB")
        print(f"圧縮後サイズ: {total_compressed / 1024 / 1024:.2f} MB")
        print(f"節約された容量: {saved_mb:.2f} MB (より率 {ratio:.2f}x)")

    for src, err in errors:
        print(f"  失敗: {src} - {err}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="画像一括圧縮ツール")
    parser.add_argument("input_dir", help="入力画像ディレクトリ")
    parser.add_argument("output_dir", help="出力ディレクトリ")
    parser.add_argument("--quality", type=int, default=75, help="JPEG/WebP品質ファクタ (1-100)")
    parser.add_argument("--max-size", type=int, default=1920, help="最大最長辺(ピクセル)")
    parser.add_argument("--format", default=None, help="ターゲットフォーマット (JPEG/WebP/PNG)")
    parser.add_argument("--no-exif", action="store_true", help="EXIFデータを破棄")
    args = parser.parse_args()

    batch_compress(
        args.input_dir,
        args.output_dir,
        quality=args.quality,
        max_size=args.max_size,
        target_format=args.format,
        keep_exif=not args.no_exif,
    )

使用例:


# 基本的なのなのなの使用メソッド
python batch_compress.py ./photos ./output

# WebPに変換、品質80、最大最長辺2560
python batch_compress.py ./photos ./output --quality 80 --max-size 2560 --format webp

このスクリプトには3つのコア設計原則があります:os.walk()を使って再度帰のに走査しディレクトリ構造を維持すること、relative_to()を使って相対パスを計算し出力レイアウトが入力と一致するようにすること、そしてPath.stat()を使ってファイファイルサイズを取得し実際の圧縮効果を計算することです。


高度テクニック

EXIF情報の保持

EXIFデータには撮影時刻、GPS座標、カメラパラメータ、絞り、シャッタースピードどのメタデータが含まれます。デフォルトではimg.save()がこの情報を破棄します。保持するには、手動で抽出して保存時に渡す必要があります:


from PIL import Image

def compress_with_exif(input_path, output_path, quality=75):
    with Image.open(input_path) as img:
        exif = img.info.get("exif", b"")  # 生のEXIFバイトを抽出
        if img.mode != "RGB":
            img = img.convert("RGB")
        img.save(
            output_path,
            format="JPEG",
            quality=quality,
            exif=exif if exif else None,  # EXIFがい場合はNoneを渡す
            optimize=True,
        )

特定のEXIFフィールドのみ保持する必要がある場合(例えばへきタグのみ)は、getexif()メソッドを使って個別に項目を選択します:


from PIL import ExifTags

def keep_orientation_only(input_path, output_path, quality=75):
    with Image.open(input_path) as img:
        exif = img.getexif()
        orientation = exif.get(0x0112)  # へきタグ
        # へきタグに基づいて画像を回変換
        if orientation:
            from PIL import ImageOps
            img = ImageOps.exif_transpose(img)
        img.save(output_path, format="JPEG", quality=quality, optimize=True)

ImageOps.exif_transpose()はEXIFへきタグに従って画像を自動的に回変換させます。これはスマートフォンで撮影された写真を処理する際に特に用です。多くの携帯電話のセンサーは横へきで取り付けられており、正しいテーブル示方へを示すためにEXIFへきタグに依存しています。

PNGからWebPへ

PNGをWebPに変換する際、画像の特性に基づいて非できる逆モードとできる逆モードを選択する必要があります。テキストやシャープ線を含む画像(アイコン、UI必要素)の場合、できる逆WebPはシャープエッジを維持します。写真コンテンツの場合、非できる逆WebPは大幅サイズの優ビット性を提供します:


from PIL import Image

def png_to_webp(input_path, output_path, lossless_threshold=0.3):
    """画像特性に基づいて非できる逆またはできる逆WebPをインテリジェントに選択"""
    with Image.open(input_path) as img:
        # 色数を数えてシンプルグラフィックかどうかを判定
        colors = img.getcolors(maxcolors=65536)
        is_simple = colors is not None and len(colors) < 256

        if is_simple:
            # 色数が少い、シャープエッジ: できる逆モード
            img.save(output_path, format="WebP", lossless=True, method=6)
            print(f"できる逆WebP (色数: {len(colors)})")
        else:
            # 色が豊富、写真: 非できる逆モード
            if img.mode != "RGB":
                img = img.convert("RGB")
            img.save(output_path, format="WebP", quality=82, method=6)
            print("非できる逆WebP (写真)")

getcolors()は色のリストまたはNone(色数がmaxcolorsを超える場合)を返します。少い色数は通常、アイコンやスクリーンショットのようシンプルグラフィックを示し、できる逆圧縮に適しています。

スマートパラメータ選択

異る画像は異る圧縮戦略に最適です。以下の関数は、解像度、透過、色数に基づいてフォーマットとパラメータを自動的に選択します:


from PIL import Image

def smart_compress(input_path, output_path):
    """画像特性に基づいて最適圧縮戦略を自動選択"""
    with Image.open(input_path) as img:
        width, height = img.size
        pixel_count = width * height
        mode = img.mode
        has_transparency = mode in ("RGBA", "LA") or (
            mode == "P" and "transparency" in img.info
        )

        # 戦略1: 透過あり -> WebP (透過とサイズのバランス)
        if has_transparency:
            if pixel_count > 2_000_000:
                q = 80
            else:
                q = 88
            img.save(output_path, format="WebP", quality=q, method=6)
            return "非できる逆WebP (透過)"

        # 戦略2: 非常ににににに大き写真 -> 縮小 + JPEG中品質
        if pixel_count > 8_000_000 or width > 4000:
            img = img.convert("RGB")
            img.thumbnail((2560, 2560), Image.Resampling.LANCZOS)
            img.save(output_path, format="JPEG", quality=75, optimize=True)
            return "JPEG (大画像を縮小)"

        # 戦略3: 通常の写真 -> JPEG高品質
        if pixel_count > 500_000:
            img = img.convert("RGB")
            img.save(output_path, format="JPEG", quality=82, optimize=True)
            return "JPEG (高品質)"

        # 戦略4: 小さい画像 -> PNGのまま保持
        img.save(output_path, format="PNG", optimize=True)
        return "PNG (小画像を保持)"

この戦略のコアロジックは、透過のある画像はWebPに、大き画像は圧縮前に縮小、通常の写真はJPEG、小さ画像は忠実度のためにPNGを使用するというものです。実際の必要条件に基づいて閾値を調整できます。


パフォーマンスの最適化

マルチスレッド処理

画像圧縮はI/OバウンドとCPUバウンドの作業が混でするタスクです。ディスクの読み書きとエンコーディングがそれぞれ時間の一部を占めます。PythonのGILは純粋CPU並列性を制限しますが、Pillowは基盤のCライブラリを呼び出してJPEG/WebPエンコーディングを行う際にGILを解放するため、マルチスレッドは大幅高速化をもたらすことができます:


import os
from pathlib import Path
from PIL import Image
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

SUPPORTED_FORMATS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"}

_print_lock = threading.Lock()


def compress_single(input_path, output_path, quality=75, max_size=1920):
    with Image.open(input_path) as img:
        exif = img.info.get("exif", b"")
        if img.mode != "RGB":
            img = img.convert("RGB")
        if max_size and max(img.size) > max_size:
            img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
        kwargs = {"quality": quality, "optimize": True, "progressive": True}
        if exif:
            kwargs["exif"] = exif
        img.save(output_path, format="JPEG", **kwargs)


def batch_compress_threaded(input_dir, output_dir, quality=75, max_size=1920,
                            workers=4):
    input_dir = Path(input_dir)
    output_dir = Path(output_dir)

    # タスクを収集
    tasks = []
    for root, _, files in os.walk(input_dir):
        for f in files:
            if Path(f).suffix.lower() in SUPPORTED_FORMATS:
                src = Path(root) / f
                rel = src.relative_to(input_dir)
                dst = output_dir / rel
                dst.parent.mkdir(parents=True, exist_ok=True)
                tasks.append((src, dst))

    total = len(tasks)
    print(f"合計{total}枚の画像、{workers}スレッドで処理\n")

    completed = 0
    total_saved = 0

    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = {
            executor.submit(compress_single, src, dst, quality, max_size): (src, dst)
            for src, dst in tasks
        }
        for future in as_completed(futures):
            src, dst = futures[future]
            try:
                future.result()
                saved = src.stat().st_size - dst.stat().st_size
                total_saved += saved
            except Exception as e:
                with _print_lock:
                    print(f"失敗: {src.name} - {e}")
            completed += 1
            with _print_lock:
                print(f"[{completed / total * 100:5.1f}%] ({completed}/{total}) {src.name}")

    print(f"\n完。節約された容量: {total_saved / 1024 / 1024:.2f} MB")


if __name__ == "__main__":
    batch_compress_threaded("./photos", "./output", quality=75, max_size=1920, workers=4)

スレッド数の経験則はCPUの論理コア数です。8コアのマシンでは、4-6スレッドで通常ほぼ最適高速化に達します。スレッドが多すぎると、ディスクI/Oの競合とメモリ圧迫によりパフォーマンスが低下することがあります。

並列性をさらに進める必要がある場合は、ProcessPoolExecutorを使ってGILをバイパスできます。トレードオフとして、プロセスはメモリを共あるできず、各プロセスがPillowライブラリを独立してロードする必要があり、起動オーバーヘッドが高くります。

メモリ管理

高解像度画像を処理する際、メモリ消費が懸念事項にります。単一の6000x4000 RGB画像はデコード後、約72MBのメモリを占有します(6000x4000x3バイト)。10枚を同時に開くと720MBにります。

メモリ管理のいくつかの重要原則:


from PIL import Image
import gc

# 1. 常にwith文を使用し、ファイルハンドルとピクセルバッファが速やかに解放されることを保証
def safe_compress(input_path, output_path, quality=75):
    with Image.open(input_path) as img:
        # 2. 変更が必要場合は画像をコピーし、オリジナルへの操作を避ける
        work = img.copy()
    # imgは解放される; workはまだスコープ内
    if work.mode != "RGB":
        work = work.convert("RGB")
    work.save(output_path, format="JPEG", quality=quality, optimize=True)
    work.close()  # 3. 明示的に閉じる

# 4. 一括処理中に定期的にガベージコレクションをトリガー
def batch_with_gc(tasks, interval=100):
    for i, task in enumerate(tasks):
        process(task)
        if i % interval == 0:
            gc.collect()

さらに、Image.MAX_IMAGE_PIXELSはデフォルトで約1億7800万ピクセル(約8900x8900)の制限があります。これを超えるとDecompressionBombErrorが発生します。本されるに大き画像を処理する必要がある場合はこの閾値を上げられますが、メモリ消費に注意してください:


Image.MAX_IMAGE_PIXELS = None  # 制限を無効化(注意して使用)

圧縮結果ので比較

以下のテーブルは、50枚のスマートフォン写真(元の平均6.5MB、4000x3000ピクセル)のセットからの実測データを示しています。すべてのテストはとじハードウェアで実行し、解像度は最長辺1920に均一に縮小しました:

品質ファクタフォーマット平均サイズより率主観の品質用途
オリジナルJPEG6.5 MB1.0xベースライン
95JPEG1.8 MB3.6xほぼと一高品質アーカイブ
85JPEG0.92 MB7.1x知覚できい差異Webサイトのヒーロー画像
75JPEG0.58 MB11.2x近距離検査でわずかアーティファクトサムネイル、リスティングページ
65JPEG0.42 MB15.5x目立つブロックアーティファクト非推奨
50JPEG0.28 MB23.2x明らか歪み極小プレビューのみ
80WebP0.51 MB12.7xJPEG 85と等モダンWebサイト
できる逆WebP4.2 MB1.5x完全にできる逆透過が必要場合

データからいくつかのパターンが浮かび上がります:

  • 品質85が費用対効果の変曲点:95とで比較してほぼ半分のサイズを節約し、品質の差は目でほぼ知覚できません。
  • 品質70を下回ると、利益が急速に減少:ファイファイルサイズは減少し続けますが、品質の低下が著しく加速します。
  • WebPの品質ファクタはJPEGと直接と等ではい:WebP 80はJPEG 85と等の品質をより小さいサイズで生成します。
  • できる逆WebPは透過が必要場合にのみ意味がある;写真の場合、サイズの優ビット性はわずかです。

FAQ

Q1: 圧縮後に画像が逆さまや横へきにテーブル示されるのはぜですか?

これは、圧縮中にスマートフォンのEXIFへきタグが破棄される一方で、実際のピクセルデータが横へきで保存されていたために発生します。解決策は、保存前にImageOps.exif_transpose(img)を呼び出すことです。これにより、EXIFへきタグに従ってピクセルデータが回変換され、画像が保存前に正しくへきます。これにより、下ストリームのソフトウェアがEXIFへき情報を読み取らくても、画像は正しくテーブル示されます。

Q2: 1000枚の画像を処理する際、メモリ使用量が増え続けるのはぜですか?

最も一般的原因は、画像オブジェクトを速やかに閉じていいことです。各画像が処理後にすぐ解放されるように、必ずwith Image.open(...) as img:コンテキストマネージャを使用してください。次に、マルチスレッド版では、すべてのタスクを一度に送信すると、Futureオブジェクトが消費されるまで結果への参照を保持し、メモリが蓄積します。解決策は、chunksizeを使ってタスクをバッチで送信するか、プロデューサー・コンシューマーのキューパターンに切りの代わりにえて並行性を制御することです。最後に、定期的にgc.collect()を呼び出すことで、循環参照によって保持されているメモリを回収できます。

Q3: PNGをJPEGに変換した後、透明領域が黒くるのはぜですか?

JPEGフォーマットは透過をサポートしません。PillowがデフォルトでRGBAをRGBに変換する際、透過背景を黒で埋めます。解決策は、まず白背景のRGB画像を作成し、次にpaste()を使ってアルファチャンネルをマスクとして元画像を重ねることです:


background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])  # 第4チャンネルがアルファ

または、ネイティブに透過をサポートするWebPに直接変換し、追加の処理しで済ませることもできます。

Q4: マルチスレッドが線形の高速化をもたらさいのはぜですか?

画像圧縮にはディスクI/OとCPUエンコーディングの両方が含まれます。スレッド数があるポイントを超えると、ディスク帯域幅がボトルネックにります。CPUはデータの読み書き速度以上には進めません。さらに、高解像度画像のデコードされたピクセルデータは大きく、異るスレッドで複数の大き画像を同時に処理すると、メモリ帯域幅の競合とキャッシュヒット率の低下を引き起こします。実際には、4-8スレッドで通常2-4倍の高速化がを得るられ、それ以上は収穫逓減おります。機械式ハードディスクには4スレッドを推奨し、NVMe SSDの場合はスレッド数をやや増やせます。


まとめ

Python Pillowによる画像の一括圧縮は3つのレイヤーに分けられます:

  • 基礎レイヤーImage.open()thumbnail()save()の3つのメソッドを習を得るし、qualityoptimizeパラメータを組み合わ使えば、単一画像を圧縮できます。これがすべての一括処理の構成必要素です。
  • エンジニアリングレイヤーos.walk()でディレクトリを走査し、Path.relative_to()で構造を維持し、concurrent.futuresで並列処理を行い、単一画像のロジックを数千枚の画像にスケールします。このレイヤーは正確性、堅牢性、スループットに焦点を受けるてます。
  • 最適化レイヤー:画像特性に基づいてフォーマットとパラメータをインテリジェントに選択します。透過画像はWebPへ、大き画像は先に縮小、小さ画像はPNGのまま。EXIF保持、へき補正、メモリ管理どの詳細が、スクリプトが本番環境で確実に実行できるかを決定します。

品質ファクタの選択には経験則があります:85は視覚のにできる逆品質の閾値、75はサイズと品質のバランスポイント、65未満はテーブル示目のには推奨されません。WebPは現でモダンブラウザで完全にサポートされており、新しいプロジェクトの優先フォーマットであるべきです。

このスクリプトの価値はプロフェッショナルツールの置き換えではく、カスタマイズ性にあります。実際のニーズに合わせて各段階を調整できます。特定のディレクトリに異るパラメータを設定し、圧縮後に自動的にCDNにアップロードし、CI/CDパイプラインに統合可能性ます。各行のコードの背後にある原理を理解することで、あらゆる画像処理シナリオに対応する基盤がを得るられます。

関連記事:

ファイル圧縮をお探しですか?SmartSlim をお試しください

独自開発のRust圧縮エンジンに基づき、PDF/画像/動画/Office/OFDど10大分類40+形式をサポート。ローカル圧縮でデータは外部に持ち出されません。