PythonとPILで、サブピクセルレンダリング縮小をする

ちょっと今更な話ですが、ライフネット生命副社長である岩瀬大輔さん著、文春新書『生命保険のカラクリ』が、全編PDFで公開されています。

これを印刷して通勤時間を利用して読もうと思ったのですが、綺麗に印刷することができないよう制限がかけられていました。なので、携帯電話で読んでやろうと思い、私の携帯電話に適したサイズに変換することにしました。
なお、私の携帯電話のスペックは、480x800の3インチ液晶です。

縮小の手順

  1. PDFをビットマップ変換
  2. ビットマップを、サブピクセルレンダリング縮小する
  3. 余白をトリミングする。トリミング結果が、480x800になるように、1,2の工程を工夫する

という手順を取ることにしました。
1のPDFビットマップ変換はGhostScriptを使用し、2と3のビットマップ画像を縮小とトリミングにはPythonとPILを使いました

PDFをビットマップに変換

色々試した結果、PDFを150dpiでラスタライズ(ビットマップ変換)した画像から余白をトリミングしたら、丁度480x800になることがわかりました。ということで、PDFからPNGへの変換は以下のようにして変換しました。

% gs -dTextAlphaBits=4 -sDEVICE=pnggray -dBATCH -q -dNOPAUSE -sOutputFile=seiho/%03d.png -r451x150 seiho.pdf

後ほどRGBのサブピクセルを計算する余地として、横方向の解像度を3倍としておきます。
変換結果:

ビットマップをサブピクセルレンダリング縮小する

単純な実装

元の画像はグレースケールにアンチエイリアシングしてあり、横方向に3倍の解像度を持っています。これを、以下のようにRGBの各サブピクセルに割り当ててみました。

  • a: ある行のグレースケールの値
  • b: それを、rgbにそのまま割り当てた場合の、サブピクセルの模式図
  • c: bのrgbの値をピクセルにしたもの

aを1/3に縮めてbのように表示するデータがc、という感じかな……。

さきほどのページをこの方法で縮小した画像がはこちら。

曲線部分は綺麗に出ていますが、縦画の左右に偽色が出ているのがわかると思います。単純に考えると、

のように見えるはずなのですが……。

左右のピクセルとの平均を取るようにした実装

エッジが目立ちすぎているので、アンチエイリアシングの基本に立ち返り、左右のピクセルとの平均を取るようにしてみました。
言葉で説明すると難しいですが、左からグレースケールの値(a)が以下のようなデータの場合、

00,55,ff,ff,ff,ff,00,00,00,00,ff,ff,ff,ff,...

左から順に、00,55,ffの平均の71、55,ff,ffの平均のc6、ff,ff,ffでff、ff,ff,ffでff、ff,ff,00でaa、としていきます.。


ということで、綺麗になりました。
携帯電話でもとても綺麗に表示されます。3インチでも問題なく読むことができます。

今回の例は、元データがモノクロのグレースケール画像だったのが、処理を非常に簡単にしています。
また、今回の実装は実装の簡単さと速度を考慮して、3ピクセルの算術平均にしましたが、アンチエイリアスの平均の取得方法には色々なアルゴリズムがあるようです。
最後に、おまけとして、Python + PILのコードを掲載しておきます。元画像や変換後のサイズに汎用性がない、私専用のものではありますが……。それにしても、PILは簡単でいいですね。

あと、言葉で説明するのが難しくて、支離滅裂でわかりにくかったらすいません。

Python + PIL のコード

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from PIL import Image, ImageChops

# 縮小後のサイズ
destSize = (480, 800)

# 元サイズでのトリミングの起点(左上原点)
geom = (240, 115)

def main(sourceFile):
    sourceImage = Image.open(sourceFile)
    shrinked = shrink(sourceImage)
    shrinked.save("out/%s" % sourceFile, "PNG")

def shrink(sourceImage):
    targetImage = Image.new('RGB', destSize)
    targetPix = targetImage.load()
    sourcePix = sourceImage.load()

    (l, t) = geom
    for y in range(destSize[1]):
        l = geom[0]
        gg = sourcePix[l-2, t] + sourcePix[l-1, t] + sourcePix[l, t]
        for x in range(destSize[0]):
            r = gg = gg - sourcePix[l-2, t] + sourcePix[l+1, t]
            g = gg = gg - sourcePix[l-1, t] + sourcePix[l+2, t]
            b = gg = gg - sourcePix[l  , t] + sourcePix[l+3, t]
            targetPix[x, y] = (r/3, g/3, b/3)
            l += 3
        t += 1
    return targetImage
        
if __name__ == "__main__":
    import sys
    for sourceFile in sys.argv[1:]:
        print sourceFile
        main(sourceFile)