ブログに載せる写真の大きさをそろえたい。商品画像を200枚まとめて縮小してほしいと頼まれた。
そんな場面に出会ったことはありませんか。
1枚や2枚なら、画像編集ソフトで開いて保存し直せば済みます。でも枚数が増えた瞬間、その作業は急に苦行へ変わります。
こういうときこそPythonの出番です。画像を開いて、縮めて、保存する。この流れをコードにしてしまえば、あとは何枚あっても同じことです。
その入り口になるのがPillowというライブラリです。この記事では、Pillowの基本と、初心者がつまずきやすいところを順番に整理していきます。
Pillowは、画像をPythonから触るための道具箱¶
まずは立ち位置から確認しましょう。Pillowは、画像ファイルを読み書きして加工するためのライブラリです。
名前が少しややこしいところがあります。もともとPILという名前のライブラリがあり、その後継として作られたのがPillowです。
オリジナルのPILは長いあいだ更新が止まっていて、いま実際に使われているのはPillowのほうです。ただし読み込むときの名前はPILのまま残っているので、ここが最初の関門になります。
インストールする名前と、コードに書く名前が違う。この一点で足止めされる人がとても多いところです。
| やること | 書く名前 |
|---|---|
| インストール | pip install pillow |
| コードでの読み込み | from PIL import Image |
| 公式が案内している対応Python | 3.10以上(Pillow 12.3.0の場合) |
pillowをimportしようとして動かない、という質問は本当によく見かけます。インストールはpillow、importはPIL。ここだけ先に覚えてしまうのが早道です。
ライブラリを追加するときは、プロジェクトごとに仮想環境を分けておくと後の管理がラクになります。【関連記事】Pythonの仮想環境(venv)って何のためにある?プロジェクトごとに混ぜない管理法
最初の3行で、画像の中身が見える¶
インストールが済んだら、まずは1枚開いてみましょう。開いて、情報を表示するだけなら3行で足ります。
from PIL import Image
img = Image.open("photo.jpg")
print(img.size) # (4032, 3024)
print(img.format) # JPEG
print(img.mode) # RGB
size は幅と高さのタプルです。高さが先だと思い込んでいると計算が逆になるので、幅が先だという点だけ意識しておいてください。
format はファイルの形式、mode は色の持ち方を表します。modeは後半でもう一度出てくるので、頭の片隅に置いておきましょう。
openした時点では、まだ読み込んでいない¶
Image.open() には、知っておくと得をする性質があります。公式ドキュメントによれば、これは遅延処理です。
ファイルが何であるかを見分けるところまでは実行しますが、実際の画素データは、加工しようとするか load() を呼ぶまで読み込まれません。
つまり、サイズだけ確認したいときはとても軽く動きます。何千枚もあるフォルダの中から大きすぎる画像を探す、といった用途で効いてくる性質です。
そのかわり、ファイルは開かれたまま残ります。大量に処理するなら with で囲むか、使い終わったら close() するのが安全です。
リサイズには2つのやり方がある¶
ここからが本題です。画像を縮める方法は1つではなく、性格の違う2つが用意されています。
resize() と thumbnail() です。どちらを使うかで結果がはっきり変わります。
from PIL import Image
img = Image.open("photo.jpg")
# 指定したサイズちょうどに変える(縦横比は無視される)
resized = img.resize((800, 600))
resized.save("resized.jpg")
# 長辺を800に収める(縦横比は保たれる)
img.thumbnail((800, 800))
img.save("thumb.jpg")
resize() は新しい画像を返します。元の画像はそのまま残るので、変数に受け取らないと結果が消えてしまいます。
一方の thumbnail() は元の画像そのものを書き換えます。返り値はないので、img = img.thumbnail(...) と書くと中身がNoneになって次の行で失敗します。
違いを表にまとめておきます。迷ったらここに戻ってきてください。
| 項目 | resize() |
thumbnail() |
|---|---|---|
| 返り値 | 新しい画像 | なし(元の画像を書き換える) |
| 縦横比 | 保たれない | 保たれる |
| 指定したサイズ | ぴったりになる | 収まる範囲に縮む |
| 拡大 | できる | 元より大きくはならない |
| 向いている場面 | サイズを厳密にそろえる | 一覧用の縮小版を作る |
一覧ページのサムネイルを作るだけなら thumbnail() で十分です。縦長も横長も混ざった写真を、まとめて同じ枠に収めてくれます。
縮小の画質は、補間方法で決まる¶
小さくするとき、Pillowは元の画素をどう間引くかを選んでいます。この選び方が resample という引数です。
公式ドキュメントでは、resize() も thumbnail() も既定はBICUBICと書かれています。写真をきれいに縮めたいなら、LANCZOSを指定すると輪郭の粗さが目立ちにくくなります。
from PIL import Image
img = Image.open("photo.jpg")
small = img.resize((800, 600), Image.Resampling.LANCZOS)
small.save("small.jpg")
古い記事を読んでいると Image.ANTIALIAS という書き方が出てきます。これはPillow 10.0.0で削除されたので、いま書くとエラーになります。
中身の処理はLANCZOSと同じものです。ネットのサンプルが動かないときは、まずここを疑ってみてください。
保存でつまずくのは、たいてい形式の違い¶
初心者が最初に出会うエラーが、保存のときのこれです。cannot write mode RGBA as JPEGというメッセージを見たことはありませんか。
原因はシンプルで、透明の情報をJPEGが持てないからです。PNGの画像はRGBAという4つの値で色を持っていて、最後のAが透明度を表します。
JPEGはRGBの3つしか扱えません。だから、そのまま渡すとPillowが断ってくるわけです。
解決策も1行です。convert("RGB") で透明の情報を落としてから保存します。
from PIL import Image
img = Image.open("logo.png") # mode は RGBA
if img.mode != "RGB":
img = img.convert("RGB")
img.save("logo.jpg", quality=85, optimize=True)
保存するファイル名の拡張子を見て、Pillowが形式を判断します。.png と書けばPNG、.jpg と書けばJPEGです。
quality はJPEGの画質で、数字が小さいほどファイルが軽くなります。85あたりが、見た目と容量のバランスが取りやすい目安です。
私は10年ほどエンジニアとして開発に関わってきましたが、この透明の落とし穴には何度も足を取られました。手元のテスト画像がすべてJPEGだったせいで気づかず、利用者がPNGのロゴを上げた瞬間に本番で落ちたことがあります。
受け取る画像の形式は選べません。だからこそ、保存の前にmodeを確認する数行を入れておくと安心です。
フォルダごと、まとめて処理する¶
Pillowの本当のうれしさは、ここから先にあります。1枚できたなら、100枚も同じです。
フォルダの中の画像を順番に読み、縮小して別のフォルダへ保存する。実務でいちばん使う形を書いてみます。
from pathlib import Path
from PIL import Image
src_dir = Path("photos")
dst_dir = Path("photos_small")
dst_dir.mkdir(exist_ok=True)
for path in src_dir.glob("*.jpg"):
with Image.open(path) as img:
img.thumbnail((800, 800))
img.save(dst_dir / path.name, quality=85)
print(f"{path.name} -> {img.size}")
保存先を別のフォルダにしているのが大事なところです。元のファイルへ上書きすると、失敗したときに戻せなくなります。
mkdir(exist_ok=True) は、フォルダが無ければ作り、あれば何もしない書き方です。実行のたびにエラーで止まるのを防いでくれます。
パスの組み立てを文字列でつなぐと、環境によって区切り文字の違いで動かなくなります。専用の道具を使うほうが安全です。【関連記事】Pythonのpathlibとは?ファイルパス操作を初心者向けに解説
スマホの写真が横向きになるのは、なぜか¶
一括処理を作ると、かなりの確率でこの現象に出会います。元の写真は縦向きなのに、変換後だけ横に倒れている。
犯人はEXIFという撮影情報です。スマホやデジタルカメラは、センサーの向きのまま画像を保存し、正しい向きをEXIFのタグとして別に記録しています。
閲覧ソフトはそのタグを読んで、表示のときに回してくれます。ところがPillowで加工して保存し直すと、そのタグが引き継がれず、倒れたままの見た目になるわけです。
対処は専用の関数を1行はさむだけです。Pillow 6.0.0から ImageOps.exif_transpose() が用意されています。
from PIL import Image, ImageOps
with Image.open("smartphone.jpg") as img:
img = ImageOps.exif_transpose(img) # 向きを実際の画素に反映する
img.thumbnail((800, 800))
img.save("fixed.jpg", quality=85)
この関数は、向きのタグに従って画像を回転させ、そのタグを取り除いた新しい画像を返します。もとに戻す情報が不要になるので、以降どのソフトで開いても同じ向きになります。
写真を扱う自動化を作るなら、開いた直後に必ずこれを通すと決めておくのがおすすめです。あとから気づいて数百枚を作り直すより、ずっと安上がりです。
Pillowでできること、他の道具に任せること¶
Pillowはリサイズ以外にもいろいろこなします。使用頻度の高いものを並べておきます。
| やりたいこと | 書き方 |
|---|---|
| 一部を切り出す | img.crop((左, 上, 右, 下)) |
| 回転させる | img.rotate(90, expand=True) |
| 左右反転 | img.transpose(Image.Transpose.FLIP_LEFT_RIGHT) |
| 白黒にする | img.convert("L") |
| ぼかす | img.filter(ImageFilter.GaussianBlur(4)) |
| 文字を書き込む | ImageDraw.Draw(img).text(...) |
crop() の4つの数字は、左上を原点とした座標です。上と下ではなく、左・上・右・下の順に渡す点にだけ注意してください。
一方で、Pillowが主役にならない領域もあります。顔の検出や動画の解析といった画像認識寄りの処理は、OpenCVのほうが向いています。
グラフを画像として書き出したいときも、Pillowで線を引くより専用のライブラリが速いです。【関連記事】Pythonのmatplotlibとは?データをグラフにして見せる基本を初心者向けに解説
作ったスクリプトを自分以外の人にも使ってもらうなら、画面を付けてしまう手もあります。【関連記事】PythonでGUIアプリは作れるの?TkinterやCustomTkinterで自分専用のツールを自作してみる
今日から使える形にまとめておく¶
ここまでの内容を、実際に手を動かすときの判断だけに絞って整理します。
インストールはpillow、importはPIL。ここでつまずいたら、この記事の最初の表に戻ってください。
縦横比を保ったまま小さくしたいなら thumbnail()、サイズを厳密に決めたいなら resize()。返り値のある無しが逆になっている点だけ、間違えないようにしましょう。
保存でエラーが出たら、まず img.mode を表示する。写真の向きがおかしかったら、exif_transpose() を思い出す。この2つで、初日のトラブルはほとんど片づきます。
そして、必ず別のフォルダへ保存する。上書きしないという原則だけは、最後まで守ってください。
まずは手元の写真を1枚開いて、img.size を表示するところから試してみてください。その数字が画面に出たら、あとは同じことを枚数ぶん繰り返すだけです。
同じ発想でExcelの定型作業も自動化できます。【関連記事】Pythonのopenpyxlとは?Excelファイルの読み書きを自動化する基本を初心者向けに解説
ここまでお読みいただきありがとうございました。