- Python
Python Pillowで画像処理|リサイズ・トリミング・形式変換のコード集
Pillowとは、Pythonで画像を開く・加工する・保存するための定番ライブラリです。 from PIL import Image で読み込み、リサイズ、トリミング、回転、形式変換、文字入れなどを数行で書けます。2026年10月時点の最新版は 12.3.0(PyPI)で、pipで入れたままの状態で JPEG・PNG・WebP・AVIF の読み書きができることを確認しました。
検索上位の記事は、リサイズだけを扱ったものや2020年の短いメモが中心でした。この記事では、よく使う操作を一覧表とコピペ用コードで網羅し、4032×3024のサンプル写真に実際に処理をかけた結果(出力サイズ・ファイル容量・形式)を表で示します。さらに RGBAをJPEG保存したときのOSError、スマホ写真の向き(EXIF)、位置情報の削除、フォルダ一括変換と所要時間 まで、実行して確かめた内容をまとめました。
Pillow操作の早見表
| やりたいこと | コード |
|---|---|
| 開く | im = Image.open("a.jpg") |
| 保存(拡張子で形式が決まる) | im.save("a.png") |
| 形式・サイズ・モード | im.format, im.size, im.mode |
| 指定サイズにする(比率無視) | im.resize((800, 600)) |
| 幅を決めて比率を保つ | im.resize((w, round(im.height * w / im.width))) |
| 枠に収める(元画像を変更) | im.thumbnail((1200, 1200)) |
| 枠に収める(新しい画像を返す) | ImageOps.contain(im, (1200, 1200)) |
| 切り抜き | im.crop((左, 上, 右, 下)) |
| 中央を指定サイズで切り抜き | ImageOps.fit(im, (1200, 630)) |
| 余白を付けて指定サイズに | ImageOps.pad(im, (600, 600), color="white") |
| 回転 | im.rotate(30, expand=True, fillcolor="white") |
| 90度単位の回転・反転 | im.transpose(Image.Transpose.ROTATE_90) / FLIP_LEFT_RIGHT |
| スマホ写真の向きを直す | ImageOps.exif_transpose(im) |
| グレースケール | im.convert("L") |
| ぼかし・シャープ | im.filter(ImageFilter.GaussianBlur(4)) / UnsharpMask() |
| 明るさ・コントラスト | ImageEnhance.Brightness(im).enhance(1.3) |
| 文字を描く | ImageDraw.Draw(im).text((x, y), "文字", font=font) |
| JPEGの画質 | im.save("a.jpg", quality=85, optimize=True) |
| WebP・AVIF | im.save("a.webp", quality=80) / im.save("a.avif", quality=60) |
| 透過PNGをJPEGに | im.convert("RGB") または白背景に貼り付け |
| EXIFを読む | im.getexif()、位置情報は .get_ifd(0x8825) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python Pillowで画像処理|リサイズ・トリミング・形式変換のコード集」
動作環境とインストール
| 項目 | バージョン |
|---|---|
| Python | 3.14.7 |
| Pillow | 12.3.0 |
| 日本語フォント | Noto Sans CJK(/usr/share/fonts/opentype/noto/) |
pip install Pillow
python -c "import PIL; print(PIL.__version__)"
# 12.3.0
パッケージ名は Pillow、importするときは PIL です。 対応形式は環境によって変わるので、使う前に次のコードで確認できます。
from PIL import features
for f in ["jpg", "webp", "avif", "freetype2", "libtiff", "jpg_2000"]:
print(f, features.check(f))
print("libavif", features.version("avif"))
jpg True
webp True
avif True
freetype2 True
libtiff True
jpg_2000 True
libavif 1.4.2
AVIFはPillow 11.2.1で読み書きに対応しました(当初はソースからビルドした場合のみ)。今回の12.3.0では、pipで入るwheelのままAVIFが使えました。features.check("avif") が False の環境では、pip install --upgrade Pillow で更新してから確認します。
開く・保存・情報取得
from PIL import Image
im = Image.open("photo.jpg")
print(im.format, im.size, im.mode, im.info.get("dpi"))
print(im.width, im.height)
im.save("photo.png") # 拡張子から PNG と判断
im.save("photo_out", format="JPEG") # 拡張子がない場合は format を指定
with Image.open("photo.jpg") as im: # 大量に開くなら with で閉じる
im.load()
| 属性 | 意味 | 例 |
|---|---|---|
im.format |
読み込んだファイル形式 | JPEG / PNG / WEBP(新規作成した画像は None) |
im.size |
(幅, 高さ) | (4032, 3024) |
im.mode |
色の持ち方 | RGB / RGBA(透過) / L(グレー) / P(パレット) |
im.info |
形式ごとの付加情報 | dpi、透過情報など |
im.getexif() |
EXIF(撮影情報) | 向き、機種、位置情報 |
Image.open() は最初はヘッダーだけを読み、画素は必要になったときに読み込みます(遅延読み込み)。そのため開いた直後でもサイズは分かり、大きな画像でもすぐ返ります。
検証用のサンプル画像と処理結果の一覧
記事に画像は載せられないため、処理結果を「出力サイズ・容量・形式」の数値で示します。 検証には、スマホ写真を想定した4032×3024のJPEG(EXIFの向き=6〈90度回転して表示〉、位置情報つき)を作って使いました。図形とノイズを混ぜて情報量を多くしているため、容量は一般的な写真より大きめに出ます。比べるときは相対的な差に注目してください。
# pil_ops.py(主要部分) 前提: photo.jpg(4032x3024、Orientation=6)がある
from pathlib import Path
from PIL import Image, ImageOps, ImageDraw, ImageFont, ImageFilter, ImageEnhance
out = Path("out"); out.mkdir(exist_ok=True)
src = Image.open("photo.jpg")
img = ImageOps.exif_transpose(src) # 向きを反映 → 3024x4032
img.resize((800, 800)).save(out / "resize_800x800.jpg", quality=85)
w = 1200
h = round(img.height * w / img.width)
img.resize((w, h), Image.Resampling.LANCZOS).save(out / "resize_w1200.jpg", quality=85)
t = img.copy(); t.thumbnail((1200, 1200)); t.save(out / "thumbnail_1200.jpg", quality=85)
img.crop((100, 200, 1100, 900)).save(out / "crop.jpg", quality=85)
side = min(img.size)
left, top = (img.width - side) // 2, (img.height - side) // 2
img.crop((left, top, left + side, top + side)).resize((600, 600)).save(out / "center_square.jpg", quality=85)
ImageOps.fit(img, (1200, 630), Image.Resampling.LANCZOS).save(out / "fit_1200x630.jpg", quality=85)
small = ImageOps.contain(img, (1200, 1200)) # 以降は 900x1200 で比較
small.rotate(30, expand=True, fillcolor="white").save(out / "rotate30.jpg", quality=85)
small.transpose(Image.Transpose.FLIP_LEFT_RIGHT).save(out / "flip.jpg", quality=85)
small.convert("L").save(out / "gray.jpg", quality=85)
small.filter(ImageFilter.GaussianBlur(4)).save(out / "blur.jpg", quality=85)
small.filter(ImageFilter.UnsharpMask(radius=2, percent=150)).save(out / "sharp.jpg", quality=85)
ImageEnhance.Brightness(small).enhance(1.3).save(out / "bright.jpg", quality=85)
for q in (95, 85, 75, 60):
small.save(out / f"q{q}.jpg", quality=q)
small.save(out / "opt_progressive.jpg", quality=85, optimize=True, progressive=True)
small.save(out / "png.png", optimize=True)
small.save(out / "webp_q80.webp", quality=80)
small.save(out / "webp_lossless.webp", lossless=True)
small.save(out / "avif_q60.avif", quality=60)
処理結果(元画像 photo.jpg:JPEG・RGB・4032×3024・1979.4 KB)
| 処理 | 出力ファイル | 形式・モード | サイズ | 容量 |
|---|---|---|---|---|
resize((800, 800)) |
resize_800x800.jpg | JPEG RGB | 800×800 | 96.0 KB |
| 幅1200で比率維持 | resize_w1200.jpg | JPEG RGB | 1200×1600 | 272.3 KB |
thumbnail((1200, 1200)) |
thumbnail_1200.jpg | JPEG RGB | 900×1200 | 154.0 KB |
crop((100, 200, 1100, 900)) |
crop.jpg | JPEG RGB | 1000×700 | 82.9 KB |
| 中央を正方形に → 600×600 | center_square.jpg | JPEG RGB | 600×600 | 54.6 KB |
ImageOps.fit((1200, 630)) |
fit_1200x630.jpg | JPEG RGB | 1200×630 | 105.9 KB |
rotate(30, expand=True) |
rotate30.jpg | JPEG RGB | 1380×1490 | 193.9 KB |
| 左右反転 | flip.jpg | JPEG RGB | 900×1200 | 153.8 KB |
| グレースケール | gray.jpg | JPEG L | 900×1200 | 117.6 KB |
| ぼかし(GaussianBlur 4) | blur.jpg | JPEG RGB | 900×1200 | 77.6 KB |
| シャープ(UnsharpMask) | sharp.jpg | JPEG RGB | 900×1200 | 217.8 KB |
| 明るさ1.3倍 | bright.jpg | JPEG RGB | 900×1200 | 173.2 KB |
| 透かし文字 | watermark.jpg | JPEG RGB | 900×1200 | 161.7 KB |
読み取れるポイント
ImageOps.exif_transpose()で、横長に保存されていた4032×3024が 縦長の3024×4032 に直ったresize((800, 800))は比率を無視して引き伸ばす。thumbnail((1200, 1200))は 長い辺を1200に合わせて比率を保つ(900×1200)rotate(30, expand=True)は回転した画像が収まるようにキャンバスが広がる(1380×1490)- ぼかすと容量が減り、シャープにすると増える。細かい模様が多いほど容量は大きくなる
リサイズ:resize・thumbnail・ImageOpsの使い分け
「比率を保って枠に収めたい」なら thumbnail か ImageOps.contain を使うのが一番簡単です。
from PIL import Image, ImageOps
s = Image.new("RGB", (300, 200))
print("resize((600,600)):", s.resize((600, 600)).size)
t = s.copy(); t.thumbnail((600, 600)); print("thumbnail((600,600)):", t.size)
print("ImageOps.contain:", ImageOps.contain(s, (600, 600)).size)
print("ImageOps.cover:", ImageOps.cover(s, (600, 600)).size)
print("ImageOps.fit:", ImageOps.fit(s, (600, 600)).size)
print("ImageOps.pad:", ImageOps.pad(s, (600, 600), color="white").size)
resize((600,600)): (600, 600)
thumbnail((600,600)): (300, 200)
ImageOps.contain: (600, 400)
ImageOps.cover: (900, 600)
ImageOps.fit: (600, 600)
ImageOps.pad: (600, 600)
| 関数 | 比率 | 拡大 | 結果の大きさ | 元画像 | 使う場面 |
|---|---|---|---|---|---|
im.resize(size) |
無視 | する | ちょうどsize | 変更しない(新しい画像を返す) | サイズが決まっている |
im.thumbnail(size) |
保つ | しない | size以内 | 元を書き換える(戻り値None) | 縮小専用、サムネイル |
ImageOps.contain(im, size) |
保つ | する | size以内 | 変更しない | 枠に収める(拡大もあり) |
ImageOps.cover(im, size) |
保つ | する | size以上 | 変更しない | 枠を埋める(はみ出す) |
ImageOps.fit(im, size) |
保つ | する | ちょうどsize(はみ出しを中央で切る) | 変更しない | OGP画像、正方形アイコン |
ImageOps.pad(im, size) |
保つ | する | ちょうどsize(余白で埋める) | 変更しない | 商品画像を白背景で統一 |
thumbnail は元画像を書き換えて None を返すため、t = im.thumbnail(...) と書くと t は None になります。元を残したいときは im.copy() してから使います。
リサンプリング(補間方法)の選び方
| 指定 | 特徴 | 向いている用途 |
|---|---|---|
Image.Resampling.NEAREST |
最速。ギザギザになる | ドット絵、ラベル画像 |
Image.Resampling.BILINEAR |
速い | プレビュー |
Image.Resampling.BICUBIC |
resize の既定 |
一般的な用途 |
Image.Resampling.LANCZOS |
縮小がきれい。やや遅い | 写真の縮小、公開用画像 |
Pillow 10以降は Image.ANTIALIAS が削除されています。 古いコードで AttributeError: module 'PIL.Image' has no attribute 'ANTIALIAS' が出たら Image.Resampling.LANCZOS に置き換えます。
トリミング:cropと中央トリミング
crop((左, 上, 右, 下)) は、左上を(0, 0)とした座標で切り抜きます。 3つ目と4つ目は「幅・高さ」ではなく「右端・下端の座標」です。
# 中央を正方形に切り抜く
side = min(img.size)
left, top = (img.width - side) // 2, (img.height - side) // 2
square = img.crop((left, top, left + side, top + side))
# 中央を 1200x630(OGP 画像の比率)で切り抜いて縮小まで一度に
ogp = ImageOps.fit(img, (1200, 630), Image.Resampling.LANCZOS)
# 中心をずらす(centering=(横, 縦)、0.0〜1.0。上寄せなら縦を 0.0)
top_ogp = ImageOps.fit(img, (1200, 630), centering=(0.5, 0.0))
範囲が画像の外にはみ出しても crop はエラーにならず、はみ出した部分は黒で埋まります。 範囲は min(right, im.width) のように画像サイズ内に収めておくと安心です。
回転・反転
| 操作 | コード | サイズ |
|---|---|---|
| 任意の角度(左回り) | im.rotate(30) |
元と同じ(四隅が切れる) |
| 任意の角度・全体を収める | im.rotate(30, expand=True, fillcolor="white") |
広がる |
| 90度(左回り) | im.transpose(Image.Transpose.ROTATE_90) |
縦横入れ替え |
| 180度 | im.transpose(Image.Transpose.ROTATE_180) |
同じ |
| 左右反転 | im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) / ImageOps.mirror(im) |
同じ |
| 上下反転 | im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) / ImageOps.flip(im) |
同じ |
rotate() の角度は反時計回り(左回り)です。 時計回りに回したいときは im.rotate(-90, expand=True) と負の値を渡します。90度単位なら、画質が変わらない transpose が向いています。
形式変換とquality・圧縮(実測)
同じ900×1200の画像を、形式とqualityを変えて保存した容量です。
| 保存方法 | 形式 | 容量 | q85 JPEG比 |
|---|---|---|---|
| JPEG quality=95 | JPEG | 317.5 KB | 206% |
| JPEG quality=85 | JPEG | 154.0 KB | 100% |
| JPEG quality=85 optimize+progressive | JPEG | 142.2 KB | 92% |
| JPEG quality=75(Pillowの既定値) | JPEG | 101.5 KB | 66% |
| JPEG quality=60 | JPEG | 69.1 KB | 45% |
| PNG optimize=True | PNG | 1131.8 KB | 735% |
| WebP quality=80 | WEBP | 56.7 KB | 37% |
| WebP lossless=True | WEBP | 904.5 KB | 587% |
| AVIF quality=60 | AVIF | 58.3 KB | 38% |
写真をWeb用に軽くするなら、WebP(quality 75〜85)かAVIFが効果的です。 今回はWebP q80がJPEG q85の約37%の容量になりました。PNGは画質を落とさない形式なので、写真には向かず、ロゴ・図・スクリーンショット向きです。
| 形式 | 主なオプション | 透過 | 向いているもの |
|---|---|---|---|
| JPEG | quality(1〜95、既定75)、optimize、progressive、subsampling |
なし | 写真 |
| PNG | optimize、compress_level(0〜9) |
あり | ロゴ、図、スクショ |
| WebP | quality、lossless、method(0〜6、大きいほど高圧縮で遅い) |
あり | Web掲載の写真・図 |
| AVIF | quality、speed |
あり | さらに軽くしたい写真 |
| GIF | save_all=True, append_images=[...] |
1色のみ | アニメーション |
JPEGの quality は95より上にしても容量が増えるだけで画質はほとんど変わらないため、公式ドキュメントも95以下を勧めています。
透かし文字を入れる(日本語フォント)
日本語を描くには、日本語を含むフォントファイルを ImageFont.truetype() で指定します。 既定フォントでは日本語が表示されません。
from PIL import Image, ImageDraw, ImageFont, ImageOps
small = ImageOps.contain(ImageOps.exif_transpose(Image.open("photo.jpg")), (1200, 1200))
font = ImageFont.truetype("/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc", 64)
wm = small.convert("RGBA")
layer = Image.new("RGBA", wm.size, (0, 0, 0, 0)) # 透明なレイヤーに描く
d = ImageDraw.Draw(layer)
text = "© サンプル株式会社"
l, t, r, b = d.textbbox((0, 0), text, font=font) # 文字の幅・高さを測る
pos = (wm.width - (r - l) - 30, wm.height - (b - t) - 40) # 右下に配置
d.text(pos, text, font=font, fill=(255, 255, 255, 140), # 半透明の白
stroke_width=2, stroke_fill=(0, 0, 0, 140)) # 縁取り
Image.alpha_composite(wm, layer).convert("RGB").save("watermark.jpg", quality=85)
| OS | 日本語フォントの例 |
|---|---|
| Linux | /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc(fonts-noto-cjk) |
| Windows | C:/Windows/Fonts/meiryo.ttc、C:/Windows/Fonts/YuGothM.ttc |
| macOS | /System/Library/Fonts/ヒラギノ角ゴシック W3.ttc |
文字サイズを測るのは textbbox() です。 以前の textsize() はPillow 10で削除されたため、古いコードで AttributeError が出たら置き換えます。半透明にするには、透明なRGBAレイヤーに描いて alpha_composite で重ねます。
よくあるつまずきと直し方(実行して再現)
RGBAをJPEGで保存するとOSError
from PIL import Image
logo = Image.open("logo.png") # 透過 PNG
print(logo.mode)
logo.save("logo.jpg")
RGBA
OSError: cannot write mode RGBA as JPEG
JPEGは透過を持てないため、RGBに変換してから保存します。 そのまま convert("RGB") すると透明部分は黒になるので、白にしたい場合は白背景に貼り付けます。
logo.convert("RGB").save("logo_black.jpg") # 透明部分は黒
bg = Image.new("RGB", logo.size, "white")
bg.paste(logo, mask=logo.getchannel("A")) # 白背景に合成
bg.save("logo_white.jpg")
print("黒:", Image.open("logo_black.jpg").getpixel((5, 5)), "白:", Image.open("logo_white.jpg").getpixel((5, 5)))
黒: (0, 0, 0) 白: (255, 255, 255)
GIFやパレットPNG(モード P)も同じで、OSError: cannot write mode P as JPEG になります。こちらも convert("RGB") で保存できます。
スマホ写真が横向きになる(EXIFの向き)
スマホ写真は、画素は横長のまま保存し、「90度回して表示して」という情報(EXIFのOrientation)を付けていることがあります。 Pillowは画素をそのまま読むため、リサイズして保存すると横倒しに見えます。
from PIL import Image, ImageOps
im = Image.open("photo.jpg")
print("size:", im.size, "Orientation:", im.getexif().get(0x0112))
fixed = ImageOps.exif_transpose(im)
print("exif_transpose:", fixed.size, "Orientation:", fixed.getexif().get(0x0112))
size: (4032, 3024) Orientation: 6
exif_transpose: (3024, 4032) Orientation: None
ImageOps.exif_transpose() は画素を正しい向きに回し、Orientationの情報も取り除きます。 開いた直後に1回呼ぶ習慣にすると、向きの問題はまとめて解決します。
EXIF(位置情報)を残す・消す
im = Image.open("photo.jpg")
exif = im.getexif()
print("GPS:", exif.get_ifd(0x8825))
im.save("save_default.jpg") # exif を渡さない
print("exif 指定なしで保存:", dict(Image.open("save_default.jpg").getexif()))
im.save("save_keep.jpg", exif=exif) # exif を渡す
print("exif=exif で保存 GPS:", Image.open("save_keep.jpg").getexif().get_ifd(0x8825))
exif2 = im.getexif()
del exif2[0x8825] # 位置情報だけ削除
im.save("save_nogps.jpg", exif=exif2)
e3 = Image.open("save_nogps.jpg").getexif()
print("GPS 削除後:", dict(e3), "GPS:", e3.get_ifd(0x8825))
GPS: {1: 'N', 2: (35.0, 40.0, 30.0), 3: 'E', 4: (139.0, 45.0, 10.0)}
exif 指定なしで保存: {}
exif=exif で保存 GPS: {1: 'N', 2: (35.0, 40.0, 30.0), 3: 'E', 4: (139.0, 45.0, 10.0)}
GPS 削除後: {272: 'SampleCam 1', 274: 6, 271: 'SampleMaker'} GPS: {}
Pillow 12.3.0でJPEGを保存したところ、exif= を渡さなければEXIFは書き込まれませんでした。 位置情報を消したいだけなら、何も渡さずに保存すれば済みます。機種名などは残して位置情報だけ消したい場合は、0x8825(GPSInfo)を削除したEXIFを渡します。exif_transpose の後でEXIFを残す場合は、元の exif ではなく fixed.getexif() を渡します(元のOrientation=6を書き戻すと、表示時にもう一度回転されるためです)。
| タグ番号 | 名前 | 内容 |
|---|---|---|
0x0112(274) |
Orientation | 表示時の向き |
0x010F(271) |
Make | メーカー |
0x0110(272) |
Model | 機種 |
0x8825(34853) |
GPSInfo | 位置情報(get_ifd で中身を取得) |
0x8769(34665) |
ExifOffset | 撮影日時など(get_ifd で取得) |
その他のエラー早見表
| エラー | 原因 | 直し方 |
|---|---|---|
OSError: cannot write mode RGBA as JPEG |
透過画像をJPEG保存 | convert("RGB") か白背景に合成 |
OSError: cannot write mode P as JPEG |
パレット画像をJPEG保存 | convert("RGB") |
UnidentifiedImageError: cannot identify image file 'note.txt' |
画像でない・壊れている・未対応形式 | try で読み飛ばす |
AttributeError: ... has no attribute 'ANTIALIAS' |
Pillow 10で削除 | Image.Resampling.LANCZOS |
AttributeError: ... has no attribute 'textsize' |
Pillow 10で削除 | draw.textbbox() |
DecompressionBombWarning / DecompressionBombError |
画素数が非常に多い画像 | 信頼できる画像なら Image.MAX_IMAGE_PIXELS を調整 |
| 日本語が四角(豆腐)になる | 日本語を含まないフォント | ImageFont.truetype("日本語フォント", size) |
thumbnail の戻り値がNone |
元画像を書き換える仕様 | 戻り値を使わず im を使う |
フォルダ内の画像を一括変換するスクリプト
次のスクリプトは「向き補正 → 長辺1600px以内に縮小 → WebPで保存」をフォルダ内の画像すべてに行います。 画像でないファイルは読み飛ばし、EXIFは付けずに保存するため位置情報も残りません。
# batch_convert.py フォルダ内の画像を「向き補正 → 長辺1600px以内 → WebP」に一括変換
# 使い方: python batch_convert.py 入力フォルダ 出力フォルダ [--workers N]
import argparse, time
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
from PIL import Image, ImageOps, UnidentifiedImageError
EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
MAX = (1600, 1600)
def convert(src: Path, dst_dir: Path) -> tuple[str, int, int]:
dst = dst_dir / (src.stem + ".webp")
try:
with Image.open(src) as im:
im = ImageOps.exif_transpose(im) # 向きを反映(EXIF は付けずに保存=位置情報も消える)
im.thumbnail(MAX, Image.Resampling.LANCZOS)
if im.mode not in ("RGB", "RGBA"):
im = im.convert("RGBA" if "transparency" in im.info else "RGB")
im.save(dst, "WEBP", quality=80, method=4)
return src.name, src.stat().st_size, dst.stat().st_size
except (UnidentifiedImageError, OSError) as e:
print(f"skip {src.name}: {e}")
return src.name, src.stat().st_size, 0
def main():
ap = argparse.ArgumentParser()
ap.add_argument("src"); ap.add_argument("dst")
ap.add_argument("--workers", type=int, default=1)
a = ap.parse_args()
src_dir, dst_dir = Path(a.src), Path(a.dst)
dst_dir.mkdir(parents=True, exist_ok=True)
files = sorted(p for p in src_dir.iterdir() if p.suffix.lower() in EXTS)
t = time.perf_counter()
if a.workers == 1:
results = [convert(f, dst_dir) for f in files]
else:
with ProcessPoolExecutor(a.workers) as ex:
results = list(ex.map(convert, files, [dst_dir] * len(files)))
el = time.perf_counter() - t
before = sum(r[1] for r in results); after = sum(r[2] for r in results)
print(f"{len(files)}枚 {el:.2f}秒(1枚あたり {el/len(files)*1000:.0f}ms) workers={a.workers}")
print(f"合計 {before/1024/1024:.1f}MB → {after/1024/1024:.1f}MB({after/before:.1%})")
if __name__ == "__main__":
main()
入力フォルダに、4032×3024のJPEG(約1.98MB)40枚、透過PNG 1枚、拡張子だけ .jpg のテキスト1つを入れて実行しました。
python batch_convert.py in webp
python batch_convert.py in webp --workers 2
skip broken.jpg: cannot identify image file 'in/broken.jpg'
42枚 22.34秒(1枚あたり 532ms) workers=1
合計 77.3MB → 3.6MB(4.7%)
skip broken.jpg: cannot identify image file 'in/broken.jpg'
42枚 20.91秒(1枚あたり 498ms) workers=2
合計 77.3MB → 3.6MB(4.7%)
| 確認項目 | 結果 |
|---|---|
| 写真の出力 | 1200×1600(縦向きに補正)、RGB、EXIFなし |
| 透過PNGの出力 | 800×400、RGBA(透過を保ったままWebP) |
| 合計容量 | 77.3MB → 3.6MB(4.7%) |
| 所要時間 | 1プロセス22.34秒、2プロセス20.91秒 |
今回の環境は2コアを他の処理と共有していたため、並列化の効果は約7%にとどまりました。 専用のコアが複数ある環境では --workers をコア数に合わせると短縮が見込めます。並列処理の使い分けは「Python並列処理|concurrent.futures使い分けと速度比較」で詳しく扱っています。
よくある質問
Q. PillowとOpenCVはどちらを使えばよいですか?
A. リサイズ・トリミング・形式変換・文字入れのような「画像ファイルの加工」はPillowが手軽です。顔検出や輪郭抽出、動画処理などの「画像解析」はOpenCVが向いています。numpy.array(im) と Image.fromarray(arr) で相互に変換できます。
Q. iPhoneのHEIC画像は開けますか?
A. Pillow単体ではHEICを開けません。pip install pillow-heif を入れ、from pillow_heif import register_heif_opener; register_heif_opener() を呼ぶと Image.open() で開けるようになります(この記事では未検証です)。
Q. 画質を落とさずに容量を減らせますか?
A. JPEGは optimize=True, progressive=True で、今回は同じ画質設定のまま約8%減りました。PNGはWebPのlossless(可逆圧縮)に変えると、今回は1131.8KBから904.5KBに減りました。
Q. DPI(解像度)を変えたいときは?
A. im.save("a.jpg", dpi=(300, 300)) で書き込めます。DPIは印刷時の大きさの情報で、画素数は変わりません。
まとめ
- 最新は12.3.0。pipで入れたままJPEG・PNG・WebP・AVIFを読み書きできた
- 比率を保つ縮小は
thumbnail(元を書き換え)かImageOps.contain、ちょうどのサイズに切るならImageOps.fit - スマホ写真は開いたらすぐ
ImageOps.exif_transpose()で向きを直す - RGBAのJPEG保存は
OSError。convert("RGB")か白背景に合成してから保存する - Web用の写真はWebP q80でJPEG q85の約37%の容量になった
- JPEGは
exif=を渡さなければEXIF(位置情報)は保存されない。残すなら必要なタグだけ渡す - 一括変換スクリプトで40枚・77.3MBを3.6MBにまとめて縮小できた
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python Pillowで画像処理|リサイズ・トリミング・形式変換のコード集」
株式会社RJCでは、Pillowを使った画像処理の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。