PNGを圧縮して高速化【Python, pngquant】

スポンサーリンク

高速表示とサイズ削減のために、PNGを減色して圧縮する方法です。パイソンからピンクォワント(pngquant.exe)を使用して圧縮します。

「pngquant」の読みですが、ユーチューブで「ピンクォワント」って呼んでる方がいらっしゃったので、そう読むことにしました。英語圏の方だったと思うのですが、その方はPNGを「ピン(グ)」って呼んでいました(グは添える感じ)。ほかにもいろいろ呼び方はあると思いますので、好きな呼び方でいきましょう。

それでこのピンクォワントですが、GUIの付いたピングー「PNGoo」というソフトも公開されています(たぶんピングーだと思います)。減色のプレビュー表示ができますので、こちらもあると便利です。16色や256色など、数字を変えて品質確認ができます。一括処理にも対応していました。

ここでは、Pythonから pngquant.exe を使用するコード例を紹介します。

pngquant.exe を取得

pngquant.exe は以下のサイトで公開されています。

pngquant
https://pngquant.org/

「Command-line」の「Binary for Windows」から「pngquant-windows.zip」を取得します。この中に「pngquant.exe」が入っています。

モジュール群のインポート

exeファイルを使用するので、subprocess関連をインポートします。マットプロットリブ(matplotlib)は、テスト画像を作るためにインポートしています。

import os
from subprocess import Popen
from subprocess import PIPE
from subprocess import TimeoutExpired
import traceback

# テスト画像を作るためのマットプロットリブ
from matplotlib import pyplot as plt

PNG圧縮関数 (pngquant.exe)

pngquant のコマンドライン文字列を生成して、実行する関数です。オプションの説明は pngquant に同梱されている「README.txt」に載っています。

基本的には、色数(ncolors)の設定だけで良いと思います。これが一番見た目とサイズに影響するオプションです。画像にもよりますが、ncolors=256(色)なら、ほぼ同じ見た目で半分以下のサイズになりました。すばらしいです。

国語辞典によると、いろかず(色数)と読むようですが、「いろすう」でも「しきすう」でもOKです。なんでもいいと思います。

その他の speed、quality、strip などは、指定しなくても大丈夫です。デフォルトの状態でも、特に問題なく圧縮してくれました。

def compress_image(
    exe_file, in_file, out_file, ncolors,
    force=None, speed=None, quality=None, strip=None,
    timeout=30):
    """画像を圧縮して保存"""

    # 上書きオプション
    if force is True:
        force_op = ' --force'
    else:
        force_op = ''

    # 出力ファイル
    output_op = ' --output "{out_file}"'.format(out_file=out_file)

    # スピードオプション
    if speed is None:
        speed_op = ''
    else:
        speed_op = ' --speed {speed}'.format(speed=speed)

    # クオリティオプション
    if quality is None:
        quality_op = ''
    else:
        quality_op = ' --quality {min}-{max}'.format(
            min=quality[0],
            max=quality[1],
            )

    # ストリップオプション
    if strip is True:
        strip_op = ' --strip'
    else:
        strip_op = ''

    # 色数オプション
    ncolors_op = ' {ncolors}'.format(ncolors=ncolors)

    # 入力ファイル
    pngfile_op = ' -- "{in_file}"'.format(in_file=in_file)

    # コマンドライン文字列生成
    cmd = '"{exe}"{force}{o}{quality}{speed}{strip}{ncolors}{i}'.format(
        exe=exe_file,
        force=force_op,
        o=output_op,
        quality=quality_op,
        speed=speed_op,
        strip=strip_op,
        ncolors=ncolors_op,
        i=pngfile_op,
        )

    # 子プロセス実行
    with Popen(
        cmd, 
        stdin=None, stdout=PIPE, stderr=PIPE,
        shell=False, universal_newlines=True) as p:
        try:
            # 子プロセスの終了を待つ
            (outs, errs) = p.communicate(timeout=timeout)
        except TimeoutExpired:
            # 子プロセスを終了
            p.kill()

            # 通信を再試行
            (outs, errs) = p.communicate(timeout=timeout)
    return (cmd, p.returncode, outs, errs)

コマンドライン文字列の例です。

"D:\\project\\tool\\pngquant\\pngquant.exe" --force --output "D:\\project\\data_plot\\000_compressed.png" --quality 0-100 --strip 64 -- "D:\\project\\data_plot\\000.png"

メイン関数

テスト画像を保存して、それを pngquant で圧縮するコード例です。

pngquant の入力ファイルと出力ファイルを同じにすれば、小さくなった画像だけが残るので、スペースの節約になります。

def main():
    """テスト"""
    # コマンドラインツールのファイルパス
    pngquant_exe = r'D:\project\tool\pngquant\pngquant.exe'

    # データフォルダ
    data_dir = r'D:\project\data_plot'

    # 入力・出力ファイル
    filename = "000.png"
    in_file = os.path.join(data_dir, filename)
    out_file = os.path.join(
        data_dir, '%s_compressed%s' % os.path.splitext(filename))

    # 子プロセスのタイムアウト(秒)
    timeout = 30

    # 作図して保存
    (fig, ax) = plt.subplots(ncols=1, nrows=1)
    try:
        # テスト画像生成
        x  = [ 1,  2,  3,  4]
        y1 = [ 3,  9, 27, 81]
        y2 = [10, 15, 20, 25]
        y3 = [15, 30, 45, 60]
        y4 = [20, 50, 65, 70]
        ax.set_xlim(0, 5)
        ax.set_ylim(0, 100)
        ax.plot(x, y1, label='y1', lw=3, alpha=0.6, marker='o', ms=8)
        ax.plot(x, y2, label='y2', lw=3, alpha=0.6, marker='o', ms=8)
        ax.plot(x, y3, label='y3', lw=3, alpha=0.6, marker='o', ms=8)
        ax.plot(x, y4, label='y4', lw=3, alpha=0.6, marker='o', ms=8)
        ax.grid()
        ax.legend(loc=2, fancybox=True)
        fig.set_dpi(100)
        fig.set_figwidth(3.6)
        fig.set_figheight(3.6)
        fig.savefig(in_file)
    finally:
        plt.close()

    # 圧縮して保存
    (cmd, returncode, outs, errs) = compress_image(
        pngquant_exe,
        in_file,
        out_file,
        ncolors=64,
        force=True,
        speed=None,
        quality=(0, 100),
        strip=True,
        timeout=timeout,
        )

    if returncode != 0:
        print('%s\n%s\n%s\n%s' % (cmd, returncode, outs, errs))
    elif errs:
        print('%s\n%s\n%s\n%s' % (cmd, returncode, outs, errs))
    return

元画像と圧縮画像の比較

元画像 (1284色)

元画像です。1284色ありました。(ファイルサイズ 29,048 バイト)

元画像 (1284色)

ncolors = 64色

64色です。(ファイルサイズ 7,088 バイト)ncolors=64色

ncolors = 8色

8色です。(ファイルサイズ 4,570 バイト)

ncolors=8色

少しギザギザした感じが出てきましたが、まだ大丈夫です。すばらしいです。

ncolors = 4色

4色です。(ファイルサイズ 3,845 バイト)ncolors = 4色

色がなくなってしまいました。このグラフだと8色までが限界のようですね。

圧縮後の色数ですが、必ずしも指定した色数を使い切るわけではないようです。 今回は指定した通りの色数になっていましたが、画像によっては ncolors=64 でも結果は61色ということもありました。ncolorsは、あくまで色数の上限のようです。

色数の調べ方

イメージマジック(ImageMagick)のコマンドラインで調べました。

ImageMagick
http://www.imagemagick.org/script/index.php

コマンドラインオプションの解説ページ
http://www.imagemagick.org/script/escape.php

「Single Letter Attribute Percent Escapes」の表にある「%k CALCULATED: number of unique colors」という機能を使用します。

コマンドライン文字列の例です。

"C:\Program Files\ImageMagick-7.0.7-Q16\magick.exe" identify -format %k "D:\project\data_plot\000_compressed_008.png"

実行すると「8」などの数字で色数が返ってきます。

業績グラフの圧縮に便利

業績チャートではたくさんのグラフを公開していますが、この画像の圧縮に pngquant.exe を使用しました。ncolors=64色です。

また、ほかにも目一杯情報を詰め込んだ業績グラフを描いたりしているのですが、その圧縮にも使用しています。1920×1080サイズのPNGだと、1枚1枚の表示がどうしてもワンテンポ挟む感じになってしまうんですね。

64色に減らすと、劇的に速くなりました。128色や256色でも十分高速化できます。あまり色を減らすと、凡例の色とグラフの色がズレてしまったりするのですが、業績グラフ程度ならあまり不便はなかったです。速くて省スペースで、とても快適になりました。

タイトルとURLをコピーしました