t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

Python Processingで写真を六角形に切り取って並べる

今回は写真を6角形に切り取って並べるPython Processingコードのメモ。

出来上がりのイメージはこんな感じ。

上記はSatisfactoryというゲームで撮りためた風景スクリーンショットである。
このゲームは最近Update 6という大型パッチがリリースされたところで、地形アップデートにより更に景観が美しくなっているので写真撮影が止まらない。

一枚つずつ紹介するのも多すぎるので小さくして並べようと思ったんだけど、パワポで縮小とトリミングを手でやり始めたところズレるしサイズも狂うしなんか微妙な見た目になってしまった。

そこでProcessingを使って綺麗にしようと思ったんだけど、どうせやるなら何か面白い形にしたいというのが発端。

コード

注意) 私の環境では画像データ38枚の読み込みに30秒ほどかかった。
大きな画像が大量に入ったフォルダーを指定するとフリーズする可能性があるのでご注意。

import glob
def setup():
    background(0)
    global radius
    radius = 87 #(1)
    size(1280, 720)
    
    #(2) (参考) https://discourse.processing.org/t/unexpected-strokes-are-shown-on-p2d-pgraphics/21660
    hexagon = createShape()
    hexagon.setStroke(False)
    hexagon.beginShape()
    for i in range(0,6): #(3)
        angle = i * PI/3 + PI/6 #(4)
        hexagon.vertex(cos(angle) * radius, sin(angle)*radius) #(5)
    
    hexagon.endShape(CLOSE)
  
    global photo
    photo = []
    
    files = glob.glob(ur"C:\Users\ho_\OneDrive\docs\My Games\FactoryGame\Screenshots\*")
    for file in files:
        print(file)
        p = loadImage(file)
        
        #(6)
        if (p.height < p.width):
            p.resize(radius*4, radius * 4 * p.height/p.width)
            cutlocation_x = p.width-radius*2
            cutlocation_y = p.height-radius
        else:
            p.resize(radius * 4 * p.width / p.height, radius*4)
            cutlocation_x = p.width-radius
            cutlocation_y = p.height-radius*2
 
        #(7) (Ref)https://forum.processing.org/two/discussion/18819/how-to-copy-a-triangle-out-of-an-image.html
        maskImage = createGraphics(p.width,p.height)
        maskImage.beginDraw()
        maskImage.shape(hexagon,cutlocation_x, cutlocation_y)
        maskImage.endDraw()
        p.mask(maskImage)

        #(8)
        p = p.get(cutlocation_x-radius, cutlocation_y-radius, radius*2, radius*2)
        photo.append(p)
    
def draw(): #(9)
    x = 0
    y = 0
    for i in photo:
        if y % 2 == 0:
            image(i, radius*1.8*x,radius*2 * 0.79 * y)
            x_limit = 8
        else:
            image(i, radius*0.9+radius*1.8*x,radius*2 * 0.79 * y)
            x_limit = 7
            
        x = x + 1
        if x == x_limit:
            x = 0
            y = y + 1

解説

  • (1) 正N角形の頂点は円に接するので、正N角形のサイズを頂点が接する円の半径radiusとして設定する。
  • (2) 頂点が円に接するということは、円周をN分割した切れ目のところがN角形の頂点になる。
  • (3) 今回は6角形なので頂点数、つまりループ回数は6回。
  • (4) 弧度法では360度が 2\pi ラジアンなので、6分割すると\frac {\pi}{3}である。これにiを掛けることで角度が求まる。また、図形を30度傾けるために\frac {\pi}{6}を足し合わせている。
  • (5) vertexはx,yを指定して図形に頂点を追加する命令である。三角関数でラジアン角度を座標に変換する。6回繰り返すと六角形の完成。
  • (6) 横長の画像か縦長の画像かを判定して縮小サイズと切り取り位置を分岐させている。

  • (7) 6角形をマスク画像としてPGraphics型のマスクイメージを作成し、画像に適用。
  • (8) マスクは画像全体に掛かるため、必要部分のみ切り取り、加工が完了したイメージをphotoリストに追加。
  • (9) xを増加しながら横に並べていき、規定数に達したらyを増やしてxをリセットする。yが偶数のときはxの最大個数を一つ減らす。

終わりに

今回はProcessingで私がやりたいことを実現できたので、今後Processingを活用するシーンが増えるという手ごたえを感じた。
また何か面白いアイデアが湧いたらProcessingを使ってみようと思う。

以上。

6/19 追記 1

指定するフォルダーによって読み込みできないバグに遭遇したので修正した。
どうやらUnicodeの文字化けと、バックスラッシュが特殊文字と解釈されていたのが原因のようで、パスの文字列にurを付けてUnicdeのraw文字列にすることで解決した。

# Before
files = glob.glob("C:\Users\ho_\OneDrive\docs\My Games\FactoryGame\Screenshots\*")

# After
files = glob.glob(ur"C:\Users\ho_\OneDrive\docs\My Games\FactoryGame\Screenshots\*")

(参考)
www.javadrive.jp

6/19 追記 2

スマホで撮影した縦長のJPEGを読み込むと横倒しになってしまう問題があることが分かった。
どうやらそれらのJPEGはもともと横倒しの画像にEXIFという方向の情報を付けることで縦向きとしてふるまっているらしく、WindowsのプレビューなどのEXIFに対応しているソフトでは縦に表示されるが、EXIFに対応していないソフトでは本来のデータどおり横向きに表示されてしまうらしい。

残念ながらProcessingはEXIFに対応してなさそうで、外部ライブラリ等を色々調べた結果断念した。
回避策として一番簡単なのはPNG形式に変換してしまうことだろうか。

私の場合はWebで一括置換するツールがあったので利用してみた。ただ本来横倒しでEXIFによって縦になっているJPEGをPNG化したら横倒しに戻ってしまったので、Windows標準のビューワーで回転させて戻した。
※このビューワーも曲者で、回転させてしばらくしないと保存されないのでサクサク回転させていくと中飛ばしになってしまうということが発生する。

6/19 追記 3

縦長の画像の場合に切り取る位置と縮尺が想定と違うことが分かった。
あんまり理論的に考えずにトライ&エラーで完成したと思い込んだ為のバグ。
これは時間を見て直そうと思う。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。