画像ファイルを扱う¶
本稿では PyOpenGL と Pillow との連携技についていくつか述べる。ここに示すプログラムは、クラス 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__), '../../_images/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_gl
で GL_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__), '../../_images/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()
メソッド
set_modelview_matrix
については クラス DeprecatedApp の実装 を参照。
実行すると以下のようなイメージ (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))