Pythonで画像処理を始めるならPillowは最有力の選択肢です。
軽量かつ簡単に学べるのに、リサイズやトリミング、文字入れ、透過合成まで多くをカバーします。
本記事では、Pillowのインストールから実用的な加工テクニック、保存のコツとエラー回避までを、Python初心者向けにていねいに解説します。
Python初心者向け Pillowの基本とインストール
Pillowとは
概要と特徴
Pillowは、旧PIL(Python Imaging Library)の後継として広く使われている画像処理ライブラリです。
数行のコードで画像の読み込みからリサイズ、編集、保存まで行えるのが最大の魅力で、学習コストが低く、実務でも十分に活躍します。
JPEGやPNGなど主要なフォーマットに対応し、色変換やフィルターも簡単に適用できます。
PILとの違い
名称が似ていますがPillowはPILのフォークで、現在はPillowが事実上の標準です。
古い記事でPILと書かれていても、現代環境ではPillowを使うと覚えておくと混乱しません。
インストール
pipでの導入
インストールは簡単で、Python公式のパッケージ管理ツールpipから一発です。
仮想環境(venv)の利用も推奨します。
# 推奨: Pythonに紐づくpipを明示
python -m pip install --upgrade pip
python -m pip install pillow
# バージョン確認
python -c "import PIL; import PIL.Image; print(PIL.__version__)"
Pillow 10以降ではリサンプリング定数が変更され、Image.ANTIALIAS
は非推奨です。
Image.Resampling.LANCZOS
など新しい定数を使いましょう。
画像を開く/保存する
基本の使い方
画像はコンテキストマネージャで開くのが安全です。
自動でクローズされ、ファイルロックの問題を避けられます。
# 基本の開く/保存する
from PIL import Image
from pathlib import Path
img_path = Path("images/input.jpg") # 実在のファイルに置き換えてください
out_path = Path("outputs/basic_copy.jpg")
out_path.parent.mkdir(parents=True, exist_ok=True)
# with構文で自動クローズ
with Image.open(img_path) as im:
print("format:", im.format) # 例: JPEG
print("mode:", im.mode) # 例: RGB, RGBA, L
print("size:", im.size) # (幅, 高さ)のタプル
# JPEGとして保存(品質を指定)
im.save(out_path, quality=90)
print("saved to:", out_path)
format: JPEG
mode: RGB
size: (4032, 3024)
saved to: outputs/basic_copy.jpg
保存時のオプションはフォーマットにより異なるため、JPEGにはquality
、PNGにはcompress_level
などを使い分けます。
画像フォーマット(JPEG/PNG)とRGB
よく使うフォーマットと色モード
フォーマットの違いと色モードの理解は加工品質と容量に直結します。
フォーマット | 透過 | 圧縮方式 | 特徴/用途 |
---|---|---|---|
JPEG | なし | 非可逆 | 写真向け。容量が小さくなりやすいが細部が劣化する。 |
PNG | あり(RGBA) | 可逆 | 透過やアイコン、文字のある画像に最適。容量はやや大きい。 |
モード | 説明 |
---|---|
RGB | フルカラー(透過なし) |
RGBA | フルカラー+アルファ(透過あり) |
L | グレースケール |
JPEGは透過を保持できません。
透過を維持したい場合はPNG(RGBA)で保存してください。
基本サンプルコード
よくあるワークフロー(開く→向き補正→幅基準リサイズ→保存)
スマホ写真のEXIF向き問題を先に解決してからリサイズすると、想定外の回転を防げます。
from PIL import Image, ImageOps
from pathlib import Path
src = Path("images/input.jpg")
dst = Path("outputs/resized_800.jpg")
dst.parent.mkdir(parents=True, exist_ok=True)
with Image.open(src) as im:
# EXIFの自動回転を反映
im = ImageOps.exif_transpose(im)
# 横幅を800pxに合わせて等比リサイズ
base_w = 800
w, h = im.size
ratio = base_w / w
new_size = (base_w, int(h * ratio))
# 高品質なLANCZOSでリサイズ
im_resized = im.resize(new_size, resample=Image.Resampling.LANCZOS)
# JPEGとして最適化して保存
im_resized = im_resized.convert("RGB") # JPEGはRGB
im_resized.save(dst, quality=90, optimize=True, progressive=True)
print(f"resized: {w}x{h} -> {im_resized.size}, saved to {dst}")
resized: 4032x3024 -> (800, 600), saved to outputs/resized_800.jpg
まずはこの例を動かし、動作確認とパス周りの理解を進めると、後の応用がスムーズです。
画像リサイズの基本
リサイズ(resize)の使い方
resizeの基本形
resize
はサイズをピクセル指定で直ちに変更するメソッドです。
アスペクト比は自動保持されないため注意します。
from PIL import Image
with Image.open("images/input.jpg") as im:
resized = im.resize((1200, 800), resample=Image.Resampling.BICUBIC) # 幅1200, 高さ800に固定
resized.save("outputs/resize_1200x800.jpg", quality=90)
print("new size:", resized.size)
new size: (1200, 800)
元の比率と異なるサイズを指定すると縦長や横長に歪みます。
等比縮小が基本です。
等比縮小
thumbnailで手軽に等比リサイズ
thumbnail
は等比を維持しつつ最大サイズ内に収める便利なメソッドです(破壊的にサイズが変わります)。
from PIL import Image
with Image.open("images/input.jpg") as im:
im.thumbnail((800, 800), resample=Image.Resampling.LANCZOS)
im.save("outputs/thumbnail_max800.jpg", quality=90)
print("thumbnail size:", im.size)
thumbnail size: (800, 600)
短辺と長辺のどちらが800になるかは元画像の比率次第です。
高品質リサイズ
リサンプリングフィルターの選択
縮小時はLANCZOS
、拡大時はBICUBIC
が無難です。
NEAREST
は高速ですがギザギザになりやすいです。
from PIL import Image
with Image.open("images/input.jpg") as im:
small_nearest = im.resize((400, 300), Image.Resampling.NEAREST)
small_bicubic = im.resize((400, 300), Image.Resampling.BICUBIC)
small_lanczos = im.resize((400, 300), Image.Resampling.LANCZOS)
small_nearest.save("outputs/small_nearest.jpg", quality=90)
small_bicubic.save("outputs/small_bicubic.jpg", quality=90)
small_lanczos.save("outputs/small_lanczos.jpg", quality=90)
print("saved 3 files with different filters")
saved 3 files with different filters
LANCZOSは文字や細線を含む画像でも滑らかになりやすい特徴があります。
横幅基準で等比リサイズ
幅だけ決めて高さは自動計算
Web用では横幅を基準に揃えるケースが多いです。
比率から高さを算出します。
from PIL import Image, ImageOps
def resize_by_width(src, dst, width=800):
with Image.open(src) as im:
im = ImageOps.exif_transpose(im)
w, h = im.size
if w <= width:
out = im.copy()
else:
ratio = width / w
out = im.resize((width, int(h * ratio)), Image.Resampling.LANCZOS)
out.convert("RGB").save(dst, quality=90, optimize=True, progressive=True)
print(f"{src} -> {dst}, {w}x{h} -> {out.size}")
resize_by_width("images/input.jpg", "outputs/width800.jpg", 800)
images/input.jpg -> outputs/width800.jpg, 4032x3024 -> (800, 600)
元画像が既に小さい場合は拡大せずそのまま保存にすると画質の劣化を防げます。
一括リサイズ
フォルダ内の画像をまとめて処理
Pathlibと再帰検索(rglob
)を組み合わせると一括処理が簡単です。
# ディレクトリ配下のJPEG/PNGを横幅800pxに等比リサイズして出力
from pathlib import Path
from PIL import Image, ImageOps
input_dir = Path("images")
output_dir = Path("outputs/800w")
target_w = 800
output_dir.mkdir(parents=True, exist_ok=True)
count_ok, count_ng = 0, 0
for p in input_dir.rglob("*"):
if p.is_file() and p.suffix.lower() in (".jpg", ".jpeg", ".png"):
try:
with Image.open(p) as im:
im = ImageOps.exif_transpose(im)
w, h = im.size
if w <= target_w:
out = im.copy()
else:
ratio = target_w / w
out = im.resize((target_w, int(h * ratio)), Image.Resampling.LANCZOS)
# JPEGで保存(透過を持つ場合は白で合成)
out_path = output_dir / f"{p.stem}_800w.jpg"
if out.mode in ("RGBA", "LA"):
bg = Image.new("RGB", out.size, (255, 255, 255))
bg.paste(out, mask=out.split()[-1])
out = bg
else:
out = out.convert("RGB")
out.save(out_path, quality=85, optimize=True, progressive=True)
print(f"Saved: {out_path.name} ({w}x{h} -> {out.size})")
count_ok += 1
except Exception as e:
print(f"Skip {p.name}: {e}")
count_ng += 1
print(f"Done. ok={count_ok}, error={count_ng}")
Saved: sample1_800w.jpg (4032x3024 -> (800, 600))
Saved: logo_800w.jpg (1200x400 -> (800, 267))
Done. ok=2, error=0
PNGの透過を保ちたい場合は出力もPNGにする必要があります。
用途に応じて出力拡張子を切り替えましょう。
画像編集・加工の基本
トリミング
任意範囲の切り出し(crop)
crop
は左上(Left, Top)から右下(Right, Bottom)の矩形座標で切り出します。
from PIL import Image
with Image.open("images/input.jpg") as im:
w, h = im.size
# 画像中央から正方形にトリミング
side = min(w, h)
left = (w - side) // 2
top = (h - side) // 2
box = (left, top, left + side, top + side)
cropped = im.crop(box)
cropped.save("outputs/crop_square.jpg", quality=90)
print("crop box:", box, "->", cropped.size)
crop box: (504, 0, 3528, 3024) -> (3024, 3024)
座標はピクセル単位の整数で指定し、範囲外を指定するとエラーやパディングになる点に注意します。
回転・反転
rotateとtranspose
rotate
は角度指定、transpose
は定型の回転や反転に使います。
from PIL import Image
with Image.open("images/input.jpg") as im:
# 30度回転(キャンバス拡張、余白は白で塗る)
rotated = im.rotate(30, expand=True, fillcolor=(255, 255, 255))
rotated.save("outputs/rotated_30.jpg", quality=90)
# 左右反転
flipped = im.transpose(Image.FLIP_LEFT_RIGHT)
flipped.save("outputs/flip_lr.jpg", quality=90)
print("rotated:", rotated.size, "flipped:", flipped.size)
rotated: (4713, 4713) flipped: (4032, 3024)
スマホ写真の向き問題はEXIFに起因のため、編集前にImageOps.exif_transpose
で補正すると安定します。
明るさ/コントラスト
ImageEnhanceで自然に補正
ImageEnhance
は明るさ、コントラスト、彩度、シャープネスを連鎖的に調整できます。
from PIL import Image, ImageEnhance
with Image.open("images/input.jpg") as im:
# 明るさを20%上げ、コントラストを10%上げる
brighter = ImageEnhance.Brightness(im).enhance(1.2)
adjusted = ImageEnhance.Contrast(brighter).enhance(1.1)
adjusted.save("outputs/bright_contrast.jpg", quality=90)
print("enhance done")
enhance done
係数1.0が元の状態で、0.0に近づくほど弱く、1.0より大きいほど強くなります。
フィルター
代表的なフィルターの適用
ぼかしやシャープ化はImageFilter
で簡単に実現できます。
from PIL import Image, ImageFilter
with Image.open("images/input.jpg") as im:
blur = im.filter(ImageFilter.GaussianBlur(radius=2))
sharp = im.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
edge = im.filter(ImageFilter.FIND_EDGES)
blur.save("outputs/gaussian_blur.jpg", quality=90)
sharp.save("outputs/unsharp.jpg", quality=90)
edge.save("outputs/edges.jpg", quality=90)
print("filters saved")
filters saved
GaussianBlurのradius
やUnsharpMaskのpercent
は画像解像度に合わせて調整します。
文字入れ
ImageDrawとフォント指定
透過レイヤを重ねてから文字を合成すると、影や枠線で視認性を高めやすくなります。
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
with Image.open("images/input.jpg").convert("RGBA") as base:
draw = ImageDraw.Draw(base)
# フォントの候補をOS毎に探す(見つからなければデフォルト)
font_candidates = [
"C:/Windows/Fonts/meiryo.ttc",
"/System/Library/Fonts/Helvetica.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
]
font_path = next((p for p in font_candidates if Path(p).exists()), None)
font = ImageFont.truetype(font_path, 48) if font_path else ImageFont.load_default()
text = "Sample Watermark"
margin = 20
# テキストのサイズを取得
bbox = draw.textbbox((0, 0), text, font=font, stroke_width=2)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
x = base.width - tw - margin
y = base.height - th - margin
# 半透明の背景+縁取り文字を重ねる
overlay = Image.new("RGBA", base.size, (0, 0, 0, 0))
o_draw = ImageDraw.Draw(overlay)
o_draw.rectangle((x - 10, y - 5, x + tw + 10, y + th + 5), fill=(0, 0, 0, 120))
o_draw.text((x, y), text, font=font, fill=(255, 255, 255, 230),
stroke_width=2, stroke_fill=(0, 0, 0, 255))
out = Image.alpha_composite(base, overlay)
out.convert("RGB").save("outputs/text_watermark.jpg", quality=90)
print("text placed at:", (x, y), "font:", font_path or "default")
text placed at: (2140, 2784) font: C:/Windows/Fonts/meiryo.ttc
日本語表示には日本語対応フォントが必要です。
環境に応じてImageFont.truetype
にフォントファイルを指定してください。
透過PNGと背景合成
ロゴ透過合成の基本
PNGのアルファチャンネルをマスクとしてpaste
やalpha_composite
で合成します。
from PIL import Image
# 背景は写真(JPEG)、ロゴは透過PNGと仮定
with Image.open("images/bg.jpg").convert("RGBA") as bg, \
Image.open("images/logo.png").convert("RGBA") as fg:
# 背景幅の20%にロゴを縮小
scale = (bg.width * 0.2) / fg.width
logo = fg.resize((int(fg.width * scale), int(fg.height * scale)), Image.Resampling.LANCZOS)
# 左上に余白を空けて貼り付け(ロゴのアルファをマスクに使用)
x, y = 20, 20
bg.paste(logo, (x, y), mask=logo)
# 最終出力をJPEGにする場合はRGBへ
bg.convert("RGB").save("outputs/composited.jpg", quality=90)
print("composited saved")
composited saved
同じサイズのRGBA画像同士ならImage.alpha_composite
も便利です。
保存のコツとよくあるエラー対策
JPEGの品質(quality)と圧縮
画質と容量のバランス
写真用途ではquality=85
前後が高画質と容量の良いバランスです。
さらにoptimize=True
でメタデータの最適化、progressive=True
で段階表示対応にするとWeb向けに有利です。
from PIL import Image
with Image.open("images/input.jpg") as im:
im = ImageOps.exif_transpose(im).convert("RGB")
im.save("outputs/q85.jpg", quality=85, optimize=True, progressive=True, subsampling=2) # 4:2:0
im.save("outputs/q95_high.jpg", quality=95, optimize=True, progressive=True, subsampling=0) # 4:4:4
print("saved q85 and q95")
saved q85 and q95
細部重視ならquality=95
+subsampling=0
、容量重視ならquality=80〜85
+subsampling=2
がおすすめです。
透過をJPEG保存できない時
RGBA→RGBへフラット化
JPEGは透過を持てないためRGBAのまま保存するとエラーになります。
背景色に合成してから保存します。
from PIL import Image
with Image.open("images/icon.png").convert("RGBA") as im:
bg = Image.new("RGB", im.size, (255, 255, 255)) # 背景は白
bg.paste(im, mask=im.split()[-1]) # アルファをマスクに合成
bg.save("outputs/icon_on_white.jpg", quality=90)
print("flattened and saved to JPEG")
flattened and saved to JPEG
透過を保持したいならPNGで保存するのが唯一の選択肢です。
ファイルパスの注意
Windowsのバックスラッシュや日本語パス
パスはPathlibで管理するとOS差異や日本語文字の扱いが安定します。
Windowsの絶対パスは生文字列(r"..."
)にするのも有効です。
from pathlib import Path
from PIL import Image
src = Path(r"C:\Users\you\Pictures\サンプル.jpg") # 生文字列で\をエスケープ不要に
dst = Path("outputs/path_test.jpg")
dst.parent.mkdir(parents=True, exist_ok=True)
with Image.open(src) as im:
im.save(dst, quality=90)
print("saved to:", dst.resolve())
saved to: /absolute/path/to/outputs/path_test.jpg
存在確認(Path.exists()
)やフォルダ作成(mkdir(exist_ok=True)
)を都度行うと、実行時エラーを大きく減らせます。
EXIFの自動回転問題への対処
ImageOps.exif_transposeで正しい向きへ
スマホの縦写真はEXIFのOrientationタグに依存しており、見かけと実サイズが一致しないことがあるため、開いた直後に補正するのが安全です。
from PIL import Image, ImageOps
with Image.open("images/phone.jpg") as im:
print("before:", im.size, "orientation:", im.getexif().get(274)) # 274はOrientation
fixed = ImageOps.exif_transpose(im)
print("after :", fixed.size)
# 可能ならEXIFを保ったまま保存
exif_bytes = im.getexif().tobytes()
fixed.convert("RGB").save("outputs/phone_fixed.jpg", exif=exif_bytes, quality=90)
print("saved fixed image with EXIF")
before: (3024, 4032) orientation: 6
after : (4032, 3024)
saved fixed image with EXIF
向きを補正しないままトリミングや文字入れを行うと、完成画像が意図せず横倒しになることがあります。
最初にexif_transpose
を適用しましょう。
まとめ
Pillowは「開く→向き補正→等比リサイズ→編集→最適化保存」までを少ない記述で完結できる強力なライブラリです。
初心者の方は、まずImage.open
とImageOps.exif_transpose
、resize
やthumbnail
の違い、保存時のquality
や透過の扱いを押さえると、Web用画像の最適化が素早く実現できます。
最後に、パスはPathlibで安全に扱い、JPEGとPNGの特性を理解して使い分けることで、画質と容量のベストバランスを得やすくなります。