2021/10/02(土) [n年前の日記]
#1 [cg_tools][python] 数字が書かれた画像をたくさん作りたい
数字が書かれた画像を ―― 「0000」から「00xx」まで連番が書かれた感じの画像を複数作りたい。ImageMagick を使えばできるんじゃないかと思えてきたので試してみた。
環境は、Windows10 x64 21H1 + Python 3.9.5 64bit + ImageMagick 7.1.0-5 Q16 x64。
環境は、Windows10 x64 21H1 + Python 3.9.5 64bit + ImageMagick 7.1.0-5 Q16 x64。
◎ 数字を描画。 :
以下のページで、ImageMagick を使って画像に文字を描画する方法が説明されてたので参考にして作業。ありがたや。
_ImageMagickで画像に文字を描画 - エラーの向こうへ
まず、指定できるフォントの種類を把握しないといけない。.ttf を指定することでもいいらしいけど…。とりあえず、以下を打てば利用できるフォントの一覧を表示できるらしい。大量にずらずらと出てきたのでテキストファイルに書き込んでエディタで参照。
実際に文字を描画してみる。以下の指定で、96x96 の画像サイズの真ん中に、「Sample」という文字を描画できる。
背景を透明にして描画したい場合は以下。
これで、一枚だけ作るやり方は分ったので、スクリプトを書いて大量に生成してみたい。
今回は、make_seq_img.py というPythonスクリプトを書いて、0000 - 0063 までの数字が書かれた64枚の画像を out/ ディレクトリに出力するようにしてみた。
_make_seq_img.py
Python では、subprocess.call(文字列) で外部プログラムを実行することができるらしい。
以下で実行。
_ImageMagickで画像に文字を描画 - エラーの向こうへ
まず、指定できるフォントの種類を把握しないといけない。.ttf を指定することでもいいらしいけど…。とりあえず、以下を打てば利用できるフォントの一覧を表示できるらしい。大量にずらずらと出てきたのでテキストファイルに書き込んでエディタで参照。
magick convert -list font magick convert -list font > fontlist.txt
実際に文字を描画してみる。以下の指定で、96x96 の画像サイズの真ん中に、「Sample」という文字を描画できる。
magick convert -size 96x96 -gravity center -font Arial-Black -fill white -background gray -pointsize 32 label:Sample output.png
- -size 96x96 : 画像サイズを指定。
- -gravity center : 画像の真ん中の位置を指定。
- -font Arial-Black : Arial-Blackフォントを指定。
- -fill white : 色は白で描画。
- -background gray : 背景色を灰色に。
- -pointsize 32 : フォントの文字サイズを指定。
- label:Sample : 「Sample」という文字列を描画させる。
背景を透明にして描画したい場合は以下。
magick convert -size 96x96 -gravity center -font Inconsolata-Bold -fill black -background rgba(0,0,0,0) -pointsize 32 label:0000 png32:output.png
- -background rgba(0,0,0,0) : 背景を透明化。もしかすると -background none でもいいのだろうか。
- png32:output.png : 出力ファイル名の先頭に png32: を追加してアルファチャンネルを含むpng画像として出力することを明示。
これで、一枚だけ作るやり方は分ったので、スクリプトを書いて大量に生成してみたい。
今回は、make_seq_img.py というPythonスクリプトを書いて、0000 - 0063 までの数字が書かれた64枚の画像を out/ ディレクトリに出力するようにしてみた。
_make_seq_img.py
import os import subprocess mgk = "magick convert" size = "-size 96x96" pos = "-gravity center" font = "-font Inconsolata-Bold" col = "-fill black" bg = "-background rgba(0,0,0,0)" pt = "-pointsize 40" for i in range(8 * 8): lbl = "%04d" % i fn = "png32:out/%04d.png" % i cmd = f"{mgk} {size} {pos} {font} {col} {bg} {pt} label:{lbl} {fn}" subprocess.call(cmd) print("Output: %s" % fn)
Python では、subprocess.call(文字列) で外部プログラムを実行することができるらしい。
以下で実行。
mkdir out python make_seq_img.py
◎ タイル状に並べて結合したいが上手く行かない。 :
これで大量の画像が生成できたので、タイル状に並べて結合してスプライトシートっぽくしてみたい。
そういった処理も ImageMagick で出来たはず、なのだけど…。
これが上手く行かない。各画像の下に、何故かファイル名っぽいものが描き込まれてしまう。何だコレ。
どうやら label なるものが描き込まれてしまっているらしい…。
-label "" (*NIX の場合は -label '' だろうか)をつけることで、label を描かない指定ができるらしいのだけど…。
たしかに label は無くなったけど、これでもまだ結果としてはよろしくない。96 x 96ドットの画像を 8 x 8個並べてるから、768 x 768ドットの画像になるはずだけど、実際は 768 x 912ドットになってしまっている。縦方向に余計な隙間が入ってるわけで。何だコレ。
以下のページを眺めてみたけど、解決方法は見つからず。
_Montage -- IM v6 Examples
以下のやりとりを眺めて、状況が分かってきた。数字画像を作成する際に label を指定したものだから、生成された画像のメタデータにも label 情報が残っていて、「この画像は label を持っている画像だな。では label も描き込まねば」と処理されてしまうようで。label を持った画像を扱うと、-label "" や -set label "" を指定しても label 用の描画領域が含まれてしまうのだとか。しかも、簡単な解決策は無いらしい。
_ImageMagick montage always includes labels - Stack Overflow
そういった処理も ImageMagick で出来たはず、なのだけど…。
magick montage -geometry +0+0 -background none out/*.png spritesheet.png
これが上手く行かない。各画像の下に、何故かファイル名っぽいものが描き込まれてしまう。何だコレ。
どうやら label なるものが描き込まれてしまっているらしい…。
-label "" (*NIX の場合は -label '' だろうか)をつけることで、label を描かない指定ができるらしいのだけど…。
magick montage -geometry +0+0 -background none -label "" out/*.png spritesheet.png
たしかに label は無くなったけど、これでもまだ結果としてはよろしくない。96 x 96ドットの画像を 8 x 8個並べてるから、768 x 768ドットの画像になるはずだけど、実際は 768 x 912ドットになってしまっている。縦方向に余計な隙間が入ってるわけで。何だコレ。
以下のページを眺めてみたけど、解決方法は見つからず。
_Montage -- IM v6 Examples
以下のやりとりを眺めて、状況が分かってきた。数字画像を作成する際に label を指定したものだから、生成された画像のメタデータにも label 情報が残っていて、「この画像は label を持っている画像だな。では label も描き込まねば」と処理されてしまうようで。label を持った画像を扱うと、-label "" や -set label "" を指定しても label 用の描画領域が含まれてしまうのだとか。しかも、簡単な解決策は無いらしい。
_ImageMagick montage always includes labels - Stack Overflow
◎ Python + Pillow でタイル状に並べて結合。 :
仕方ないので、ImageMagick で結合するのは諦めて、Python 3.9.5 64bit + Pillow 8.3.2 で画像結合するスクリプトを書いた。スクリプト内にマジックナンバーがその他が列挙されちゃってるけど…。まあ、処理ができればいいか…。
_joint_image.py
_output.png
一応これで、数字が書かれている画像をタイル状に並べることができた。
GIMP 2.10.22 x64 Portable samj版を使って、背景に少し模様っぽいものも追加してみた。96 x 96 ドットの画像を作ってから、フィルタ → カラーマッピング → 並べる、で、800% x 800% にして並べて、前述の画像と重ね合わせた。
以下が完成画像。CC0 / Public Domain ってことで。まあ、どこでこんなの使うんだよって感じだけど…。
_spritesheet_number.png
ということで、ImageMagick の挙動でハマったものの、なんとか目的は果たせた。
それにしても、結局 Python スクリプトを書いて対応したわけだから…。だったら最初から、数字を描き込む処理も含めて、全部 Python + Pillow で処理してしまえばよかったのではないか…な…。
_joint_image.py
from PIL import Image, ImageFilter import glob inputfiles = "out/*.png" output = "output.png" xloop = 8 l = sorted(glob.glob(inputfiles)) w, h = 0, 0 imgs = [] for s in l: im = Image.open(s) if im.size[0] > w: w = im.size[0] if im.size[1] > h: h = im.size[1] imgs.append(im) yloop = len(l) // xloop if len(l) % xloop != 0: yloop += 1 tw = w * xloop th = h * yloop nim = Image.new("RGBA", (tw, th), (0, 0, 0, 0)) xi, yi = 0, 0 idx = 0 while yi < yloop: x, y = xi * w, yi * h nim.paste(imgs[idx].copy(), (x, y)) idx += 1 if idx >= len(l): break xi += 1 if xi >= xloop: xi = 0 yi += 1 nim.save(output)
_output.png
一応これで、数字が書かれている画像をタイル状に並べることができた。
GIMP 2.10.22 x64 Portable samj版を使って、背景に少し模様っぽいものも追加してみた。96 x 96 ドットの画像を作ってから、フィルタ → カラーマッピング → 並べる、で、800% x 800% にして並べて、前述の画像と重ね合わせた。
以下が完成画像。CC0 / Public Domain ってことで。まあ、どこでこんなの使うんだよって感じだけど…。
_spritesheet_number.png
ということで、ImageMagick の挙動でハマったものの、なんとか目的は果たせた。
それにしても、結局 Python スクリプトを書いて対応したわけだから…。だったら最初から、数字を描き込む処理も含めて、全部 Python + Pillow で処理してしまえばよかったのではないか…な…。
[ ツッコむ ]
#2 [pygame] Pyagmeで画像を分割して描画
Python + Pygame で、スプライトシート画像を読み込んで、各パーツに分割してウインドウ内に描画したい。考えてみたら、今までそういう処理は試してなかったな…。さて、どうすれば…。
環境は、Windows10 x64 21H1 + Python 3.9.5 64bit + pygame 2.0.1。
使用画像は以下。
_spritesheet_number.png
環境は、Windows10 x64 21H1 + Python 3.9.5 64bit + pygame 2.0.1。
使用画像は以下。
_spritesheet_number.png
◎ blit()を使う方法。 :
Pygame は、Surface.blit() を使うことで、Surface から Surface へと画像を転送することができる。この blit() に pygame.Rect という矩形範囲を渡すことで、元画像の任意の範囲だけを転送、ということができるらしい。
これを使えばスプライトシート画像の一部だけを抜き出して描画できるんじゃないかな…。ということで、試してみた。
_02_blit_mn.py
pygame.Rect(x, y, width, height) で矩形範囲を指定できる。それを配列 rects に蓄えておいて…。スクリーンに相当する Surface、を格納している screen に blit() で描画する際に、area=rects[index] という形で渡して、「元画像の、この範囲だけを抜き出して描画せよ」的な処理をしている。
実行したら、こうなった。
元画像の一部だけを描画できている。
ということで、blit() に pygame.Rect を渡せば、目的は果たせると分かった。
ちなみに、上記のスクリプトでは pygame.Rect() を使ってるけど、その後ググってみたら、blit(img, (px, py), (x, y, w, h)) という書き方もできると知った。
処理速度で違いはあったりするのかな…。どうなんだろう。
これを使えばスプライトシート画像の一部だけを抜き出して描画できるんじゃないかな…。ということで、試してみた。
_02_blit_mn.py
import pygame import sys SCRW, SCRH = 320, 240 pygame.init() screen = pygame.display.set_mode((SCRW, SCRH), pygame.DOUBLEBUF) imgname = "spritesheet_number.png" img = pygame.image.load(imgname).convert_alpha() # make split area list rects = [] w, h = 96, 96 for y in range(8): for x in range(8): rects.append(pygame.Rect(x * w, y * h, w, h)) index = 0 running = True clock = pygame.time.Clock() # Main loop while running: # check event for ev in pygame.event.get(): if ev.type == pygame.QUIT: running = False if ev.type == pygame.KEYDOWN: if ev.key == pygame.K_ESCAPE or ev.key == pygame.K_q: # Push ESC or Q key running = False screen.fill((40, 60, 200)) # clear screen # draw image x = 160 - 96 / 2 y = 120 - 96 / 2 screen.blit(img, (x, y), area=rects[index]) index = (index + 1) % len(rects) pygame.display.flip() clock.tick_busy_loop(60) cap = "Image Split - %5.2f FPS" % (clock.get_fps()) pygame.display.set_caption(cap) pygame.quit() sys.exit()
pygame.Rect(x, y, width, height) で矩形範囲を指定できる。それを配列 rects に蓄えておいて…。スクリーンに相当する Surface、を格納している screen に blit() で描画する際に、area=rects[index] という形で渡して、「元画像の、この範囲だけを抜き出して描画せよ」的な処理をしている。
実行したら、こうなった。
元画像の一部だけを描画できている。
ということで、blit() に pygame.Rect を渡せば、目的は果たせると分かった。
ちなみに、上記のスクリプトでは pygame.Rect() を使ってるけど、その後ググってみたら、blit(img, (px, py), (x, y, w, h)) という書き方もできると知った。
# make split area list rects = [] w, h = 96, 96 for y in range(8): for x in range(8): rects.append((x * w, y * h, w, h)) # ... # draw image x = 160 - 96 / 2 y = 120 - 96 / 2 screen.blit(img, (x, y), rects[index])
処理速度で違いはあったりするのかな…。どうなんだろう。
◎ subsurface() を使う方法。 :
Pygame の Surface には、subsurface() というメソッドがあって、コレを使うと、元画像の一部を新たな Surface として得ることができるらしい。引数として、やはり pygame.Rect を渡す模様。
ということで試してみた。
_03_subsurface.py
Rect を配列に蓄える代わりに、subsurface を配列 imgs に蓄えている。また、screen に blit() で描画する際、元画像として subsurface を渡している。
実行したら、こうなった。
ということで、subsurface を使っても、元画像の一部を切り出した感じで描画できると分かった。
ただ、Surface のドキュメントを読むと、ハードウェア描画がどうのこうの、みたいなことが書いてあるような…。
_pygame.Surface - pygame v2.0.1.dev1 documentation
ディスプレイモードがハードウェア描画じゃない場合は、表示に使ってる Surface から subsurface を作ることもできる、と言ってるのかな…。逆に言うと、ハードウェア描画の場合は表示に使ってる Surface から subsurface を作れませんよ、ということだろうな…。
ということで試してみた。
_03_subsurface.py
import pygame import sys SCRW, SCRH = 320, 240 pygame.init() screen = pygame.display.set_mode((SCRW, SCRH), pygame.DOUBLEBUF) imgname = "spritesheet_number.png" img = pygame.image.load(imgname).convert_alpha() # make subsurface imgs = [] w, h = 96, 96 for y in range(8): for x in range(8): _rect = pygame.Rect(x * w, y * h, w, h) imgs.append(img.subsurface(_rect)) index = 0 running = True clock = pygame.time.Clock() # Main loop while running: # check event for ev in pygame.event.get(): if ev.type == pygame.QUIT: running = False if ev.type == pygame.KEYDOWN: if ev.key == pygame.K_ESCAPE or ev.key == pygame.K_q: # Push ESC or Q key running = False screen.fill((40, 60, 200)) # clear screen # draw image x = 160 - 96 / 2 y = 120 - 96 / 2 screen.blit(imgs[index], (x, y)) index = (index + 1) % len(imgs) pygame.display.flip() clock.tick_busy_loop(60) cap = "Image Split - %5.2f FPS" % (clock.get_fps()) pygame.display.set_caption(cap) pygame.quit() sys.exit()
Rect を配列に蓄える代わりに、subsurface を配列 imgs に蓄えている。また、screen に blit() で描画する際、元画像として subsurface を渡している。
実行したら、こうなった。
ということで、subsurface を使っても、元画像の一部を切り出した感じで描画できると分かった。
ただ、Surface のドキュメントを読むと、ハードウェア描画がどうのこうの、みたいなことが書いてあるような…。
_pygame.Surface - pygame v2.0.1.dev1 documentation
ディスプレイモードがハードウェア描画じゃない場合は、表示に使ってる Surface から subsurface を作ることもできる、と言ってるのかな…。逆に言うと、ハードウェア描画の場合は表示に使ってる Surface から subsurface を作れませんよ、ということだろうな…。
[ ツッコむ ]
以上、1 日分です。