Pythonで作る「JPEG高圧縮スクリプト」

IT

モグラ先生: 今日は画像ファイルの奥底に眠るムダなバイトを「掘って見つけた!」するぞい!


はじめに — 今回掘るテーマ📜

ある日、もぐTech開発室の共有フォルダがパンパンに。
「画像を圧縮したはずなのにまだデカい…!」と嘆く新人モグラくん。
そこへ現れたのが モグラ先生

モグラ先生: CompressJPEG ってサイト並みに圧縮できるツール、ローカルで掘り当ててみようじゃないか!

こうして始まった 「Python × MozJPEG で高圧縮 JPEG スクリプトを作る冒険」
この記事では、その発掘記録をコミカルにまとめます 🪹


1. なぜ圧縮してもサイズが減らないの?🤔

  • ブログ用に画質を落とさずロスレス圧縮 → せいぜい 7〜10 % 減
  • CompressJPEG は 品質 80〜85 に再エンコード → 30〜60 % 減

モグラ先生: 簡単に言うとね、ロスレスは“土をならす”だけ、ロッシーは“石を砕く”からよりスリムになるわけさ。

小さな穴掘りポイント

モグラ先生:

  • ロスレス:画素そのまま、安全だけど削減少。
  • 視覚的ロスレス:人の目にバレにくい範囲で量子化し直す。これが CompressJPEG の正体だ!

2. モグラ流アプローチ🛠️ — MozJPEGを採用

なぜ MozJPEG?

  • FacebookやWikipediaも採用する OSS エンコーダ
  • cjpeg -quality 85CompressJPEG とほぼ同じテーブルを吐く
  • Windows ならバイナリを置くだけで動く(ビルド不要)
mozjpeg/
├─ cjpeg.exe      ← 高圧縮用
└─ jpegtran.exe   ← ロスレス最適化用(フォールバック)

モグラ先生: 公式バイナリは https://mozjpeg.codelove.de/binaries.html からゴソッと採掘じゃ!


3. 完成スクリプトを公開📜

from __future__ import annotations
import sys, subprocess, pathlib, tempfile, shutil, os
from typing import Sequence
from tqdm import tqdm

# ---------- 設定 ----------
QUALITY = 85          # CompressJPEG 相当
SSIM_THRESHOLD = 0.98 # これを下回ったら警告
# ------------------------

# SSIM 評価関数
def ssim_metric(orig: pathlib.Path, new: pathlib.Path):
    try:
        from skimage.metrics import structural_similarity as ssim
        from skimage.io import imread
        img1 = imread(orig)
        img2 = imread(new)
        if img1.ndim == 3:
            img1 = img1.mean(axis=2)
        if img2.ndim == 3:
            img2 = img2.mean(axis=2)
        data_range = max(img1.max(), img2.max()) - min(img1.min(), img2.min())
        score, _ = ssim(img1, img2, full=True, data_range=data_range)
        return score
    except Exception:
        return None

# ---------------- MozJPEG ラッパ ----------------

def _run(cmd: Sequence[str]):
    subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def compress_with_cjpeg(src: pathlib.Path, dst: pathlib.Path):
    """高圧縮モード (lossy, quality=85)"""
    _run([
        "cjpeg", "-quality", str(QUALITY), "-optimize", "-progressive",
        "-outfile", str(dst), str(src)
    ])

def compress_with_jpegtran(src: pathlib.Path, dst: pathlib.Path):
    """ロスレス最適化フォールバック"""
    _run([
        "jpegtran", "-copy", "none", "-optimize", "-progressive",
        "-outfile", str(dst), str(src)
    ])

# ---------------- メイン ----------------

def compress_image(path: pathlib.Path):
    out_path = path.with_stem(path.stem + "_compressed")
    tmp = pathlib.Path(tempfile.mktemp(suffix=".jpg"))

    # まず cjpeg (高圧縮) を試みる
    try:
        compress_with_cjpeg(path, tmp)
        mode = "cjpeg"
    except FileNotFoundError:
        # cjpeg.exe が無ければ jpegtran にフォールバック
        compress_with_jpegtran(path, tmp)
        mode = "jpegtran"

    # 結果評価
    orig_size = path.stat().st_size
    new_size = tmp.stat().st_size
    improvement = 100 * (1 - new_size / orig_size)
    score = ssim_metric(path, tmp)

    shutil.move(tmp, out_path)

    quality_report = (
        f"{path.name}: {improvement:.1f}% 減 (mode={mode})" +
        (f", SSIM={score:.4f}" if score is not None else "") +
        (" ←⚠" if score is not None and score < SSIM_THRESHOLD else "")
    )
    print(quality_report)


def main(argv: Sequence[str]):
    if len(argv) == 0:
        print("JPEG ファイルをこのスクリプトにドロップしてください。")
        return

    for arg in tqdm(argv, desc="Compressing"):
        p = pathlib.Path(arg)
        if p.suffix.lower() not in (".jpg", ".jpeg"):
            print(f"スキップ: {p.name} は JPEG ではありません")
            continue
        try:
            compress_image(p)
        except subprocess.CalledProcessError as e:
            print(f"{p.name}: MozJPEG での処理に失敗しました → {e}")
        except Exception as e:
            print(f"{p.name}: 処理失敗 → {e}")

if __name__ == "__main__":
    main(sys.argv[1:])

小さな穴掘りポイント

モグラ先生:

  • SSIM 0.98 未満なら ⚠️ を出して画質チェックだ。
  • cjpeg.exe が無いと自動でロスレスに逃げる保険付き!

4. セットアップ手順💻

  1. MozJPEGバイナリを配置
    cjpeg.exejpegtran.exe をスクリプトと同じフォルダへ置く
  2. Python依存をインストール
   pip install pillow scikit-image tqdm
  1. ドラッグ&ドロップで実行
   python compress_jpeg.py sample.jpg

sample_compressed.jpg が爆誕!

モグラ先生: PyInstaller で EXE にすると Windows でもっとラクに掘れるぞい!


5. 実際に掘ってみた!🔍

画像元サイズスクリプト (-Q85)削減率
test.jpg1.2 MB550 KB-54 %
hero.jpg3.4 MB1.6 MB-52 %

モグラ先生: 肉眼で比べても違いはほぼ分からず。掘り出し成功!


6. さらなる穴掘り(応用編)🪄

  • 品質をもっと落として良ければ QUALITY = 80 で -60 % 超えも狙える
  • PNG も対象にしたいなら pngquant を組み合わせる
  • CI で自動圧縮 → GitHub Actions で mozjpeg-action を使うと便利

モグラ先生: 「落とし穴」はcjpeg が遅いこと。大量バッチなら先にリサイズしてから圧縮だぞ!


おわりに🎉

今回の冒険で、CompressJPEG と肩を並べるローカルスクリプトを掘り当てました。
ブログ執筆やサイト運営で「画像が多くて容量が…」と悩むモグラーは、ぜひ試してみてください!

モグラ先生: さぁ次はどんなお宝を掘り当てようか? みんなのリクエスト待ってるモグ〜! 🐾⛏️

タイトルとURLをコピーしました