画像ファイルを扱う

本稿では PyOpenGLPillow との連携技についていくつか述べる。ここに示すプログラムは、クラス DeprecatedApp を派生することで構築しているので、先に クラス DeprecatedApp の実装 を読んでおきたい。

PNG ファイルからテクスチャーを生成する

既存の PNG ファイルから 2 次元テクスチャーデータを生成する方法の一例を示す。

ポイントは Pillow の Image インスタンスのメソッド tostring の戻り値を関数 glTexImage2D に渡すことだ。アルファチャンネルを含む PNG ファイルからテクスチャーデータを作成する例の、コード全景を示す。

#!/usr/bin/env python
"""texturedemo.py: Demonstrate how to use OpenGL texture with PIL."""
# pylint: disable=unused-argument, no-self-use, invalid-name
import sys
import os
import OpenGL.GL as GL
from PIL import Image
from deprecatedapp import DeprecatedApp

class TextureDemoApp(DeprecatedApp):
    """Demonstrate how to use OpenGL texture (bitmap)."""

    def __init__(self, **kwargs):
        """Initialize an instance of class TextureDemoApp."""

        kwargs['context_version'] = (1, 5)
        super(TextureDemoApp, self).__init__(**kwargs)

    def init_gl(self):
        """Initialize the OpenGL state."""

        GL.glClearColor(0.0, 0.0, 0.0, 1.0)
        GL.glEnable(GL.GL_DEPTH_TEST)
        GL.glEnable(GL.GL_TEXTURE_2D)

    def init_object(self):
        """Initialize the object to be displayed."""

        vx, vy = (40.0, 40.0)
        tx, ty = (6.0, 6.0)

        vertices = (
            -vx, -vy, 0.0,
            vx, -vy, 0.0,
            vx, vy, 0.0,
            -vx, vy, 0.0,)

        texcoords = (
            -tx, -ty,
            tx, -ty,
            tx, ty,
            -tx, ty,)

        colors = (
            0, 0, 0, 0.75,
            0, 0, 0, 0.75,
            1, 1, 1, 0.75,
            0, 0, 0, 0.75,)

        GL.glVertexPointer(3, GL.GL_FLOAT, 0, vertices)
        GL.glTexCoordPointer(2, GL.GL_FLOAT, 0, texcoords)
        GL.glColorPointer(4, GL.GL_FLOAT, 0, colors)

    def init_texture(self):
        """Initialize textures."""

        source_path = os.path.join(
            os.path.dirname(__file__), '../../_static/illvelo.png')
        img = Image.open(source_path).resize((256, 256))
        assert img.mode == 'RGBA'

        GL.glTexImage2D(
            GL.GL_TEXTURE_2D, 0, GL.GL_RGBA,
            img.size[0], img.size[1],
            0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, img.tobytes())

        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
        GL.glTexEnvf(
            GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_DECAL)

    def do_render(self):
        """Render the object."""

        GL.glPushAttrib(GL.GL_CURRENT_BIT)
        GL.glMatrixMode(GL.GL_MODELVIEW)
        GL.glPushMatrix()
        self.set_modelview_matrix()

        GL.glPushClientAttrib(GL.GL_CLIENT_VERTEX_ARRAY_BIT)
        GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
        GL.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY)
        GL.glEnableClientState(GL.GL_COLOR_ARRAY)

        GL.glDrawArrays(GL.GL_POLYGON, 0, 4)

        GL.glPopClientAttrib()
        GL.glPopAttrib()
        GL.glPopMatrix()

def main(args):
    """The main function."""

    app = TextureDemoApp(
        window_title=b"Build texture from a PNG file",
        window_size=(320, 240),
        camera_eye=(14, 14, 1.58,),
        camera_up=(0, 0, 1))
    app.run(sys.argv)

if __name__ == "__main__":
    sys.exit(main(sys.argv))

以下、ポイントを絞って解説する。

メソッド __init__

旧式 OpenGL アプリケーション用クラス DeprecatedApp のサブクラスを定義する。

        """Initialize an instance of class TextureDemoApp."""

        kwargs['context_version'] = (1, 5)
        super(TextureDemoApp, self).__init__(**kwargs)

  • 新しいサンプルコードを書くのが面倒なので、昔書いたレガシー API を使ったコードをリファクタリングする。その都合上、コンテキストバージョンを 1.5 にしたい。それ以外はスーパークラスの既定値に従う。

メソッド init_gl

        """Initialize the OpenGL state."""

        GL.glClearColor(0.0, 0.0, 0.0, 1.0)
        GL.glEnable(GL.GL_DEPTH_TEST)
        GL.glEnable(GL.GL_TEXTURE_2D)

メソッド init_glGL_TEXTURE_2D 機能を有効にしておくことを忘れずに。

メソッド init_object

メソッド init_object をオーバーライドすることで、描画オブジェクトを定義する。

        """Initialize the object to be displayed."""

        vx, vy = (40.0, 40.0)
        tx, ty = (6.0, 6.0)

        vertices = (
            -vx, -vy, 0.0,
            vx, -vy, 0.0,
            vx, vy, 0.0,
            -vx, vy, 0.0,)

        texcoords = (
            -tx, -ty,
            tx, -ty,
            tx, ty,
            -tx, ty,)

        colors = (
            0, 0, 0, 0.75,
            0, 0, 0, 0.75,
            1, 1, 1, 0.75,
            0, 0, 0, 0.75,)

        GL.glVertexPointer(3, GL.GL_FLOAT, 0, vertices)
        GL.glTexCoordPointer(2, GL.GL_FLOAT, 0, texcoords)
        GL.glColorPointer(4, GL.GL_FLOAT, 0, colors)

空間座標・テクスチャー座標・色からなる頂点データをレガシー API で定義する。

メソッド init_texture

        """Initialize textures."""

        source_path = os.path.join(
            os.path.dirname(__file__), '../../_static/illvelo.png')
        img = Image.open(source_path).resize((256, 256))
        assert img.mode == 'RGBA'

        GL.glTexImage2D(
            GL.GL_TEXTURE_2D, 0, GL.GL_RGBA,
            img.size[0], img.size[1],
            0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, img.tobytes())

        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
        GL.glTexEnvf(
            GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_DECAL)

PNG ファイルからテクスチャーを作成している(Pillow 利用ノート 参照)。メソッド tostring で矩形イメージの RGBA バイト列を得られるということが本質的だ。イメージのピクセルサイズが OpenGL 的に中途半端なので、決め打ちだが 256 ピクセル四方にリサイズする。

ファイルパスが私のノート環境から決まる値に決め打ちになっているが、あくまでも本稿はテクスチャー描画の実現方法に主眼があるので気にしない。

〆に関数 glTexImage2D 呼び出しにより、テクスチャーデータを OpenGL に渡している。残りのテクスチャーオプションは、アプリケーションの目的に応じてパラメーターを指定すればよい。

メソッド do_render

メソッド do_render をオーバーライドすることで描画処理の中心部分を定義する。レガシー API のオンパレードなので、説明は省く。

        """Render the object."""

        GL.glPushAttrib(GL.GL_CURRENT_BIT)
        GL.glMatrixMode(GL.GL_MODELVIEW)
        GL.glPushMatrix()
        self.set_modelview_matrix()

        GL.glPushClientAttrib(GL.GL_CLIENT_VERTEX_ARRAY_BIT)
        GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
        GL.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY)
        GL.glEnableClientState(GL.GL_COLOR_ARRAY)

        GL.glDrawArrays(GL.GL_POLYGON, 0, 4)

        GL.glPopClientAttrib()
        GL.glPopAttrib()
        GL.glPopMatrix()

実行すると以下のようなイメージ (320x240) を得るだろう。

イルベロ床

日本語テキストを描画する

次に Pillow のテキスト描画機能と PyOpenGL のテクスチャー機能を活用したプログラムの例を示す。当然ながら先述の例とプログラムの構造が同じであることを利用して、クラス TextureDemoApp からさらなるサブクラス TextDemoApp を定義することでコード作業の手間を省く。

#!/usr/bin/env python
"""textdemo.py: Demonstrate how to use OpenGL texture with PIL."""
# pylint: disable=unused-argument, no-self-use, invalid-name
import sys
import OpenGL.GL as GL
from texturedemo import TextureDemoApp
from texture import draw_text

class TextDemoApp(TextureDemoApp):
    """Demonstrate how to use OpenGL texture with PIL."""

    def init_object(self):
        """Initialize the object to be displayed."""

        vx, vy = (5.0, 5.0)

        vertices = (
            -vx, vy, 0.0,
            -vx, -vy, 0.0,
            vx, -vy, 0.0,
            vx, vy, 0.0,)

        texcoords = (
            0, 0,
            0, 1,
            1, 1,
            1, 0)

        colors = (
            1, 1, 1, 1,
            0.5, 0.5, 0.5, 1,
            0.5, 0.5, 0.5, 1,
            1, 1, 1, 1,)

        GL.glVertexPointer(3, GL.GL_FLOAT, 0, vertices)
        GL.glTexCoordPointer(2, GL.GL_FLOAT, 0, texcoords)
        GL.glColorPointer(4, GL.GL_FLOAT, 0, colors)

    def init_texture(self):
        """Initialize textures."""

        img = draw_text('潔')

        GL.glTexImage2D(
            GL.GL_TEXTURE_2D, 0, GL.GL_RGBA,
            img.size[0], img.size[1],
            0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, img.tobytes())

        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
        GL.glTexParameterf(
            GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)

def main(args):
    """The main function."""

    app = TextDemoApp(
        window_title=b"Build texture from text",
        window_size=(320, 240),
        camera_eye=(0.5, -9.5, 1.58,),
        camera_center=(0.5, 10.0, 1.58,),
        camera_up=(0., 0., 1.))
    app.run(sys.argv)

if __name__ == "__main__":
    sys.exit(main(sys.argv))

各メソッドの概要は前項のものと同様だが、メソッド init_texture の最初の処理は説明が必要だ。

テキストを画像化する

今度は既存の PNG ファイルからではなく、システムフォントからイメージを生成する。生成処理は別のプログラムでも利用する可能性が高いため、別途関数 draw_text に定義する。

#!/usr/bin/env python
"""texture.py: Provide a helper function.
"""
from PIL import (Image, ImageDraw, ImageFont)

def draw_text(text, initsize=256, point=144, bgcolor='black', forecolor='white'):
    """Helper function."""

    # Create a larger canvas.
    img = Image.new('RGBA', (initsize, initsize), bgcolor)
    draw = ImageDraw.Draw(img)
    fnt = ImageFont.truetype('HGRME.TTC', point)

    ext = draw.textsize(text, font=fnt)
    draw.text((0, 0), text, font=fnt, fill=forecolor)

    # (left, upper, right, lower)-tuple
    imgcr = img.crop(
        (0, 0,
         ext[0], ext[1] + 16))

    # TODO: Use the power of two value that is the closest
    # to each component of img.size.
    return imgcr.resize((initsize, initsize), Image.ANTIALIAS)

この処理の説明は Pillow 利用ノート 参照。

PIL は難しくて、例えばメソッド crop を素直に textsize の戻り値で切り落とすと、境界線が文字に若干かぶるらしく、いやな位置でカットしてしまう。そこでプラス 16 などという、やってはいけない類の補正を施している。

以上を実行すると、実行結果のスクリーンショットはだいたい次のようなものになる。

潔

Pillow + glReadPixels によるスクリーンショット取得

ウィンドウに描画されているイメージをファイルに保存できるとたいへん便利なので、次のような関数を定義しておくとよい。この関数をクラス AppBase のキーボードイベントコールバックあたりから呼び出すようにしておくと便利。

def capture_screen():
   sx = glutGet(GLUT_WINDOW_WIDTH)
   sy = glutGet(GLUT_WINDOW_HEIGHT)

   pixels = glReadPixels(0, 0, sx, sy, GL_RGBA, GL_UNSIGNED_BYTE)
   img = Image.fromstring("RGBA", (sx, sy), pixels)
   img = img.transpose(Image.FLIP_TOP_BOTTOM)

   filename = input('filename: ')
   img.save(filename)
   print('{} saved'.format(filename))