モグラ先生: 今日は画像ファイルの奥底に眠るムダなバイトを「掘って見つけた!」するぞい!
はじめに — 今回掘るテーマ📜
ある日、もぐTech開発室の共有フォルダがパンパンに。
「画像を圧縮したはずなのにまだデカい…!」と嘆く新人モグラくん。
そこへ現れたのが モグラ先生。
モグラ先生: CompressJPEG ってサイト並みに圧縮できるツール、ローカルで掘り当ててみようじゃないか!
こうして始まった 「Python × MozJPEG で高圧縮 JPEG スクリプトを作る冒険」。
この記事では、その発掘記録をコミカルにまとめます 🪹
1. なぜ圧縮してもサイズが減らないの?🤔
- ブログ用に画質を落とさずロスレス圧縮 → せいぜい 7〜10 % 減
- CompressJPEG は 品質 80〜85 に再エンコード → 30〜60 % 減
モグラ先生: 簡単に言うとね、ロスレスは“土をならす”だけ、ロッシーは“石を砕く”からよりスリムになるわけさ。
小さな穴掘りポイント
モグラ先生:
- ロスレス:画素そのまま、安全だけど削減少。
- 視覚的ロスレス:人の目にバレにくい範囲で量子化し直す。これが CompressJPEG の正体だ!
2. モグラ流アプローチ🛠️ — MozJPEGを採用
なぜ MozJPEG?
- FacebookやWikipediaも採用する OSS エンコーダ
cjpeg -quality 85
で CompressJPEG とほぼ同じテーブルを吐く- 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. セットアップ手順💻
- MozJPEGバイナリを配置
cjpeg.exe
とjpegtran.exe
をスクリプトと同じフォルダへ置く - Python依存をインストール
pip install pillow scikit-image tqdm
- ドラッグ&ドロップで実行
python compress_jpeg.py sample.jpg
→ sample_compressed.jpg
が爆誕!
モグラ先生: PyInstaller で EXE にすると Windows でもっとラクに掘れるぞい!
5. 実際に掘ってみた!🔍
画像 | 元サイズ | スクリプト (-Q85) | 削減率 |
---|---|---|---|
test.jpg | 1.2 MB | 550 KB | -54 % |
hero.jpg | 3.4 MB | 1.6 MB | -52 % |
モグラ先生: 肉眼で比べても違いはほぼ分からず。掘り出し成功!
6. さらなる穴掘り(応用編)🪄
- 品質をもっと落として良ければ
QUALITY = 80
で -60 % 超えも狙える - PNG も対象にしたいなら pngquant を組み合わせる
- CI で自動圧縮 → GitHub Actions で
mozjpeg-action
を使うと便利
モグラ先生: 「落とし穴」はcjpeg が遅いこと。大量バッチなら先にリサイズしてから圧縮だぞ!
おわりに🎉
今回の冒険で、CompressJPEG と肩を並べるローカルスクリプトを掘り当てました。
ブログ執筆やサイト運営で「画像が多くて容量が…」と悩むモグラーは、ぜひ試してみてください!
モグラ先生: さぁ次はどんなお宝を掘り当てようか? みんなのリクエスト待ってるモグ〜! 🐾⛏️