mieki256's diary



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。

数字を描画。 :

以下のページで、ImageMagick を使って画像に文字を描画する方法が説明されてたので参考にして作業。ありがたや。

_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 で出来たはず、なのだけど…。

magick montage -geometry +0+0 -background none out/*.png spritesheet.png

これが上手く行かない。各画像の下に、何故かファイル名っぽいものが描き込まれてしまう。何だコレ。

spritesheet_0.png


どうやら label なるものが描き込まれてしまっているらしい…。

-label "" (*NIX の場合は -label '' だろうか)をつけることで、label を描かない指定ができるらしいのだけど…。

magick montage -geometry +0+0 -background none -label "" out/*.png spritesheet.png

spritesheet_1.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
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
_output.png

一応これで、数字が書かれている画像をタイル状に並べることができた。

GIMP 2.10.22 x64 Portable samj版を使って、背景に少し模様っぽいものも追加してみた。96 x 96 ドットの画像を作ってから、フィルタ → カラーマッピング → 並べる、で、800% x 800% にして並べて、前述の画像と重ね合わせた。

以下が完成画像。CC0 / Public Domain ってことで。まあ、どこでこんなの使うんだよって感じだけど…。

spritesheet_number.png
_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

blit()を使う方法。 :

Pygame は、Surface.blit() を使うことで、Surface から Surface へと画像を転送することができる。この blit() に pygame.Rect という矩形範囲を渡すことで、元画像の任意の範囲だけを転送、ということができるらしい。

これを使えばスプライトシート画像の一部だけを抜き出して描画できるんじゃないかな…。ということで、試してみた。

_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] という形で渡して、「元画像の、この範囲だけを抜き出して描画せよ」的な処理をしている。

実行したら、こうなった。

02_blit_mn_ss01.gif

元画像の一部だけを描画できている。

ということで、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
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 を渡している。

実行したら、こうなった。

03_subsurface_ss01.gif

ということで、subsurface を使っても、元画像の一部を切り出した感じで描画できると分かった。

ただ、Surface のドキュメントを読むと、ハードウェア描画がどうのこうの、みたいなことが書いてあるような…。

_pygame.Surface - pygame v2.0.1.dev1 documentation

ディスプレイモードがハードウェア描画じゃない場合は、表示に使ってる Surface から subsurface を作ることもできる、と言ってるのかな…。逆に言うと、ハードウェア描画の場合は表示に使ってる Surface から subsurface を作れませんよ、ということだろうな…。

以上、1 日分です。

過去ログ表示

Prev - 2021/10 -
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project