Pillow 12.1.0 の PNG 保存が592.98msかかった話
この記事の見出し
Pillow 12.1.0 で6000x4000のランダム画像を扱うと、PNG保存の中央値が592.98msになってしまった。
いちばん面食らったのはリサイズそのものではなく、1920px幅へ縮小してから保存しているPNGが、6000x4000のまま保存したJPEGより重かった点だった(画素数を減らした側が重い、という見え方になる)。
JPEG保存は184.42msなので、PNG保存は3.22倍かかっている。普通に考えると、画素数を減らしてから書き出すほうが軽く見える(ここだけ見ると、かなり直感に反する)。
この環境では逆になり、測定日は2026年6月16日だった(この時点では、保存形式と素材の相性をまだ切り分けられていなかった)。タイムスタンプは2026-06-16T20:18:00だった。マシンは Surface Pro 11th Edition、SoC は Snapdragon X 12-core X1E80100、OS は Windows 11 Pro 10.0.26200 の ARM64 版。Python は3.12.10で、Pillow は12.1.0を使った。
ランダム画像を同じ素材にした
6000x4000のランダム画像を作り、同じ入力から処理ごとに3回ずつ時間を測った。比較したのは LANCZOS と BILINEAR の縮小、90度回転、グレースケール変換、JPEG保存、PNG保存である。保存処理だけはファイル形式の圧縮が入るので、単純なメモリコピーとは別物として読む必要がある(この時点で、あとから比べにくい条件を少し混ぜている)。
実際に見ていた処理は、だいたい次の形で、記事側では説明用に短くしている(元のスクリプトでは同じ入力を使い回し、保存だけは形式ごとの圧縮が入るので、後から比べると少し混ざった条件になる)。
from PIL import Image
img = Image.fromarray(random_rgb_6000x4000)
small_lanczos = img.resize((1920, 1280), Image.Resampling.LANCZOS)
small_bilinear = img.resize((1920, 1280), Image.Resampling.BILINEAR)
rotated = img.rotate(90, expand=True)
gray = img.convert("L")
img.save("out.jpg", quality=85)
small_lanczos.save("out.png")
元画像は2400万画素で、中央値を元画素数で割ると、LANCZOSは1画素あたり9.75ns、BILINEARは3.92ns、90度回転は3.23ns、グレースケール変換は1.84nsになる(公平な比較ではなく、ざっくりした物差し)。JPEG保存は7.68ns、PNG保存は24.71nsという元画像換算だ。PNGは実際には縮小後の画像を書いているので、この割り算だけで公平な比較にはならない。それでも、どの処理が支配的かをつかむには具体的になる。
保存だけが別の重さを持つ
3回分をそのまま置く。中央値だけを見ると見えなくなる揺れがあるため、runs_ms も残した。
| 処理 | runs_ms | median_ms |
|---|---|---|
| resize_lanczos_1920 | 215.54 / 293.73 / 233.89 | 233.89ms |
| resize_bilinear_1920 | 85.09 / 94.18 / 138.70 | 94.18ms |
| rotate_90 | 77.62 / 104.38 / 70.18 | 77.62ms |
| to_grayscale | 56.06 / 44.15 / 39.20 | 44.15ms |
| save_jpeg_q85 | 170.00 / 196.83 / 184.42 | 184.42ms |
| save_png_1920 | 623.08 / 572.05 / 592.98 | 592.98ms |
出力サイズは jpeg_bytes が18,107,176バイト、png_bytes が6,195,646バイトだった。ここで注意したいのは、JPEGは6000x4000のまま、PNGは1920x1280に縮小後という条件の違いを見る必要があること。バイト数だけ見るとPNGのほうが小さいが、縮小後のPNGは245万7600画素しかない。1画素あたりでは約2.52バイトになる。JPEGは2400万画素で18,107,176バイトなので、約0.75バイト。ランダムノイズ相手のPNGがかなり苦しいことは、この差だけでも見える。
LANCZOS と BILINEAR の差
リサイズは、LANCZOSが233.89ms、BILINEARが94.18msだった。差は2.48倍で、画質のために LANCZOS を選ぶ場面は多いが、サムネイルを大量に作る処理ではこの差がそのまま待ち時間へ乗る。
個人的には、ここがいちばん実運用へつながる数字だった。1本や2本の画像なら233.89msでも気になりにくい。けれど1000本を処理するバッチでは、同じ縮小でも LANCZOS を選ぶだけで積み上がってしまう。BILINEARの94.18msが許せる見た目なら、先にそちらを試してみる価値がありそうだ。
ただし、今回の入力はランダム画像だった。写真、スクリーンショット、図版ではキャッシュの効き方も圧縮のされ方も変わってくる。Pillow の Resampling だけを見たいなら、保存とは切り離して測った今回の表で十分だった。画質と容量まで含めた判断には材料が足りない状態になっている。
PNG が遅すぎる理由と測定の失敗
この測定は、PNG保存の評価としては失敗で、何を同じにして何を変えたのかをそろえないまま形式名だけを並べると、面白い逆転は出ても、次に自分が使える判断材料にはなりにくい(ここは後から読み直して痛いところ)。何を同じにして、何を変えたのかを揃えないまま保存形式だけを比べると、面白い数字は出ても、あとで自分が使える判断材料にはなりにくい。
ランダムノイズはPNGにとってかなり悪い素材だった。PNGは近い色や繰り返しを利用して小さくする形式なので、隣の画素と関係が薄いランダム画像では圧縮が効きにくい。実際に png_bytes は6,195,646バイトまで膨らんだ。縮小後でこの大きさなら、圧縮器が楽をできていないと見るほうが自然だろう。
さらに悪いのは、JPEGとPNGで保存前の画素数をそろえていないこと。JPEGは6000x4000のまま、PNGは1920x1280へ縮小後である。処理時間だけを見ると「小さいPNGが大きいJPEGより遅い」という面白い逆転になるが、これは形式差と入力差を混ぜていて、保存形式だけを比べたように読むとかなり危ない(後から見返して一番直したい箇所)。比較実験としては間違いだった(気づくまで、かなり危ない読み方をしていたし、後から見ると条件のそろえ方が甘い)。
この失敗に気づくまで、最初はPNGが単純に遅いという話に寄せそうになった。けれど jpeg_bytes と png_bytes を見比べると、少なくとも「PNG保存は常にこのくらい遅い」と書くのは無理がある。スクリーンショットのように同じ色が広く続く画像なら、まったく違う結果になるはずだ。なぜランダム画像だけで代表値のように読んでしまったのか、そこは後から見てもかなり危ない。次に測るなら、写真、UIスクリーンショット、単色の多い図版を分ける。
回転とグレースケールは素直だった
90度回転は77.62ms、グレースケール変換は44.15msだった。このあたりは驚くほどの話ではない。回転は画素の並べ替え、グレースケールは各画素の変換なので、メモリ帯域と実装の素直な勝負に見える。
2400万画素を44.15msでグレースケール化するなら、元画像換算で1画素あたり1.84ns。90度回転の77.62msは3.23ns。Surface Pro 11th Edition の Snapdragon X でこの程度なら、画像1枚の前処理としては軽い部類に入る。少なくとも、今回の表ではPNG保存の592.98msや LANCZOS の233.89msに比べると主役ではない。
使い分けをどう変えるか
手元のスクリプトで大量のサムネイルを作るなら、まず保存形式とリサイズ方式を分けて測っておきたい。LANCZOS と BILINEAR の2.48倍差は、見た目の許容ラインを決める材料になる。PNG保存の592.98msは強烈だが、この数字だけでPNGを避けるのは危ない(素材がランダムノイズだった影響が大きい)。今回の入力がランダムノイズだったせいで、PNGに不利な条件を自分で作っている(形式の比較をしたつもりで、実際には素材の圧縮しにくさも一緒に測ってしまった)。
運用としては、写真系はJPEGを基準にし、UI画像や透過が必要な素材だけPNGを検討対象に回す。サムネイル生成では BILINEAR で下書きを作り、必要な箇所だけ LANCZOS を使う。そういう雑な二段構えでも、処理本数が増えると効く(毎回最高画質で全画像を通すより、先に用途で分けるほうが待ち時間を説明しやすい)。
今回の収穫は、Pillow 12.1.0 が ARM64 Windows で普通に動いたことより、測り方の粗さが数字にそのまま出たことだった。次はランダムノイズではなく、実際にサイトで使う画像を混ぜて測り直してみる。