Blenderではじめる画像処理

> 本記事は [Blender Advent Calendar 2016](http://www.adventar.org/calendars/1571)　12/08 分として寄稿したものです。

Blender上の画像データをPythonで扱う際の基本的な方法と`NumPy`や`PIL(Pillow)`との併用の仕方について、またBlender Python特有の微妙なハマりどころ等を書いていきます。

## はじめに

3DCG統合環境であるBlenderには、3Dだけでなく二次元画像を扱うためのコンポジター/イメージペイント機能も用意されてあります。通常はレンダリングの出力を加工したり、モデルのテクスチャを描くといった具合に3D機能に従属したものとして使用されることの多いこれらの機能ですが、純粋に二次元画像を扱うためのツールとしても活用でき、見方によってはBlenderは画像編集ソフトにもなり得ます。

ところで、Blenderは`Python`を内蔵しており、PythonスクリプトによってBlenderを制御することができる仕組みが用意されています。また同じくスクリプトによって今開いているBlenderファイルの使用している様々な内部データへもアクセスができ、その中には画像データも含まれます。ということは、Blenderは`Pythonで画像処理を行うための環境`にもなり得るということを意味します。

最低限の画像処理を行うには、画像の入力、画像データの持つピクセルデータの読み書き、および処理を施した画像の出力(どれもBlender上で完結)ができればいいので、本記事ではそれを実現するための方法を書いていくことにします。

(ちなみに使用しているBlenderのバージョンは`2.78`です)


## Blender上でPythonコードを実行する方法の簡単なおさらい

Blender上でPythonスクリプトを実行するには`Text Editor`か`Python Console`を使用します。これらのエディタを出現させる最も簡単な方法は、標準で用意されている`Scripting`レイアウトを呼び出すことでしょう：

![image](https://qiita-image-store.s3.amazonaws.com/0/152179/9964bfb8-c6d2-e2c0-f8f7-1b7c64c9ce93.png)

このような画面に切り替わります。

![image](https://qiita-image-store.s3.amazonaws.com/0/152179/cfcc154f-b6be-9244-7a8f-370b08e958c1.png)

Pythonコードを`Text Editor`の中に書いた後`Run Script`ボタンを押す(もしくはそのショートカットキーである`Alt+P`キーを押す)ことで実行が行えます。

```python
print('hello bpy world!')
```

![image](https://qiita-image-store.s3.amazonaws.com/0/152179/cb5dc7ad-9bcc-cd71-0a18-205683e543e1.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/152179/8b586e56-09ba-11ac-f183-eee464450d81.png)


`print()`での文字列の出力はBlenderのコンソールウィンドウに対して行われるため、コンソールウィンドウが表示されていない場合は予めBlenderメニューの`Window > Toggle System Console`を選択して表示させておいた方が良いでしょう。

`Text Editor`とは別に`Python Console`からもスクリプトの実行が行えます。こちらはCtrl+スペースキーでオートコンプリートが行えるので、Blender APIのデータ構造を調べる際に便利です。しかしながら(不可能ではないものの)複数行のコードの実行が行いにくいという問題があるため、簡単なテストを行う時に使用すると良いと思います。


## 画像を用意する

基本的に画像に関する事柄は`UV/Image Editor`を通して行います。

![newimage-selectimageeditor](https://qiita-image-store.s3.amazonaws.com/0/152179/505bfc02-7470-4ffa-b5e0-cc124214ff7e.png)

![newimage-imageeditor.png](https://qiita-image-store.s3.amazonaws.com/0/152179/b8088e27-95be-13c7-0e69-8d87886d10d6.png)
<font color="DarkGray">UV/Image Editor</font>


### 外部画像を読み込む

UV/Image Editorのヘッダメニューの `Image > Open Image` からローカルにある既存の画像ファイルをBlenderに読み込めます。

![newimage-open](https://qiita-image-store.s3.amazonaws.com/0/152179/c4617e95-476b-7b7b-3503-fcfc6e5d3366.png) ![newimage-openedimage](https://qiita-image-store.s3.amazonaws.com/0/152179/25507e73-556f-08c7-3343-fe6bd0fad80d.jpeg)


ファイル選択後、当該画像がBlender内部で利用できる形になります。


### 新規に画像を作成する

UV/Image Editorのヘッダメニューの `Image > New Image` から新規画像作成ダイアログが表示されます。

![newimage-new](https://qiita-image-store.s3.amazonaws.com/0/152179/97caa5f2-8c38-069e-5aa2-6b500aadb064.png) ![newimage-dialog](https://qiita-image-store.s3.amazonaws.com/0/152179/2582ae26-4e00-55d1-4a7c-675bb033a3b8.png)


画像名・画像サイズ諸々を入力した後`OK`を押すことでBlender内に新たに画像が作成されます。
UV/Image Editorには簡易的なペイント機能もついているので0から自前でテスト用画像を描くことも可能です。

![newimage-selectpaint](https://qiita-image-store.s3.amazonaws.com/0/152179/3e991e1d-50d5-8dcc-0a08-de54c339d24e.png) ![newimage-paint](https://qiita-image-store.s3.amazonaws.com/0/152179/a91b5a77-6322-9793-ff73-36a308ca80d0.png)



<font color="White">.</font>

ちなみに新規画像作成の際、`Generated Type`で`Color Grid`を選択するとテスト用の画像が生成されます。手頃な画像が無い場合には非常に便利です。

![newimage-typecolorgrid](https://qiita-image-store.s3.amazonaws.com/0/152179/d89cd4fa-68a6-00f4-a255-6a9986ba8c77.png)


![newimage-colorgrid.png](https://qiita-image-store.s3.amazonaws.com/0/152179/6ae8b7b4-86f3-4e7d-305d-f24daa0847d4.png)
<font color="DarkGray">このような画像が自動で作成されます</font>



### レンダリング結果を使用する

基本的に上の2つのケースとあまり違いは無いのですが、Blender Pythonから利用する際の特有の問題がいくつかある([後述](#レンダリング結果の画像のピクセルにアクセスする際の注意点))のでここでは一旦置いておきます。
なので初めは読み込んだ外部画像か新規で作成した画像を使用してください。


## Blender Pythonで画像の操作を行う

既に用意された画像をPythonスクリプトによって操作する方法を記していきます。なお、テスト用の画像のサイズはあまり大きすぎない値(最大でも 128x128 程度 )にしておくことをオススメします。

### Blenderが使用している画像データにアクセスする

`bpy.data.images[<画像名>]`から今開いているBlenderファイルが使用している画像データにアクセスできます。次のコードは対象画像の横幅・縦幅を表示します。



```python:画像サイズを表示
import bpy
blimg = bpy.data.images['A']
width, height = blimg.size
print(width, height)
```

![image](https://qiita-image-store.s3.amazonaws.com/0/152179/c2c75990-66a0-9c62-9561-2113ad2cac36.png)

`.size`に画像の横幅と縦幅が格納されています。
`<画像名>`の箇所は`UV/Image Editor`でその画像に割当てられている名前と同じになります。


### ピクセル情報を取得・セットする

```python
import bpy
blimg = bpy.data.images['A']

# ピクセル情報を取得
print(blimg.pixels[0], blimg.pixels[1], blimg.pixels[2], blimg.pixels[3]) # R,G,B,A (1ピクセル目)
print(blimg.pixels[4], blimg.pixels[5], blimg.pixels[6], blimg.pixels[7]) # R,G,B,A (2ピクセル目)

# ピクセル情報をセット
blimg.pixels[0] = 1.0
blimg.pixels[1] = 0.0
blimg.pixels[2] = 0.0
blimg.pixels[3] = 1.0
# => 1ピクセル目が"赤色"に設定される
```

`.pixels`からその画像のピクセル値の配列にアクセスできます。

![pixelstructure](https://qiita-image-store.s3.amazonaws.com/0/152179/a96ea13c-697e-5678-f0a7-080ad8a7b2a1.png)


ピクセル配列は画像の**左下**から1点ごとにR,G,B,Aの4つの画素値が並んでおり、そのため配列全体のサイズは`width*height*4`になります。
またそれぞれの画素値は浮動小数点数で、通常なら最小値が`0.0`，最大値が`1.0`で表現されます。(`0～255`の整数でないことに注意。) また0.0～1.0の範囲を逸脱しても許容されます。


### ピクセル配列を一括で取得・セットする

各ピクセルに対してイテレーションを行う際、上述のやり方では実はパフォーマンスに問題が出てきます。<font color="DarkGray">( 細かい話になりますが、`.pixels`はただの配列でなくアクセサとなっていて、例えば`blimg.pixels[i]=...`のように代入を行うごとにBlender内部で更新処理が走ってしまいます。)</font>
そこで一旦`.pixels`をただの配列として一括で取得しておいて、それに対して処理を加えた後、再び`.pixels`にセットするという流れで進めることにします。

```python
import bpy
blimg = bpy.data.images['A']
width, height = blimg.size

pxs = list(blimg.pixels[:]) # 全ピクセル情報をただの配列として一括で取得

for i in range(0, width*height*4, 4):
    pxs[i]   = 1.0 # R
    pxs[i+1] = 0.0 # G
    pxs[i+2] = 0.0 # B
    pxs[i+3] = 1.0 # A

blimg.pixels = pxs # 処理を加えた配列を一括でセット
```

![setpixels-1](https://qiita-image-store.s3.amazonaws.com/0/152179/d3f65021-8c43-2638-2ac5-1ad4ff7de0ed.png)
<font color="DarkGray">実行結果</font>

また新たに設定するピクセル配列を1から作成しておいて、それを代入するというやり方もあります。


```python:元々のピクセルを参照しつつ新たなピクセルを作成
import bpy
blimg = bpy.data.images['A']
width, height = blimg.size

pxs0 = blimg.pixels[:]
pxs = [0] * len(pxs0) # もしくは pxs = [0] * (width * height * 4)

for i in range(0, width*height*4, 4):
    pxs[i]   = pxs0[i] * 0.5   # R
    pxs[i+1] = pxs0[i+1] * 0.5 # G
    pxs[i+2] = pxs0[i+2] * 0.5 # B
    pxs[i+3] = pxs0[i+3]       # A

blimg.pixels = pxs
```

![setpixels-3](https://qiita-image-store.s3.amazonaws.com/0/152179/c97155f9-5799-4fc2-83a4-b22f6e9413f6.png)


<font color="DarkGray">**※(細かい注意点)** 上のようにオリジナルのピクセルを参照するだけの場合でも、`.pixels`でなく`.pixels[:]`のようにして純粋なリスト(もしくはタプル)の形式に変換してからアクセスしておいた方が無難です。既に述べたアクセサの問題で、`.pixels`のままアクセスするとパフォーマンスが極めて低下することがあります。</font>



### 座標を指定してピクセル値をセット

ピクセルのx, y座標を指定して値の取得/セットを行う場合、その配列のインデックス値は`(y*width+x)*4`となります。座標を直接指定する場合、その座標が画像のサイズ内にきちんと収まっているかも確認しておく必要があります。

次の例では画像の中の(x, y) = (10, 20)～(10, 40)の範囲の矩形を白色で塗りつぶします。

```python
import bpy
blimg = bpy.data.images['A']
width, height = blimg.size

pxs = list(blimg.pixels[:])

for y in range(10, 40):
    for x in range(10, 20):
        if 0<=x and x<width and 0<=y and y<height: # x,yが画像内に収まっているかを確認
            i = (y*width+x)*4
            pxs[i]   = 1.0 # R
            pxs[i+1] = 1.0 # G
            pxs[i+2] = 1.0 # B
            pxs[i+3] = 1.0 # A

blimg.pixels = pxs
```

![setpixels-2](https://qiita-image-store.s3.amazonaws.com/0/152179/8b41e267-5bc3-31c0-a43a-79cbbbd589a2.png)




### `Box Blur`を愚直に実装してみる

最低限ピクセル操作を行うことが可能となったので、ここいらで少し演習をしてみます。一番単純な*ぼかし*のメソッドである`Box Blur`を実装してみましょう。簡単のために画像のアルファ値は全て`1.0`になっているものとします。

Box Blurは対象ピクセル1点ごとに、その周辺の矩形の範囲にある全ピクセルの平均値を新たなピクセルの値として採用すれば良いので、愚直に実装すると次のようになります：

```python
# <!> 簡単のために画像のアルファ値は常に1.0とする

import bpy
blimg = bpy.data.images['A']
width, height = blimg.size

pxs0 = blimg.pixels[:]
pxs = [0] * len(pxs0)

def inside(x,y):
    return 0<=x and x<width and 0<=y and y<height

size = 5
for y in range(height):
    for x in range(width):
        i = (y*width+x)*4
        r=0
        g=0
        b=0
        n=0
        for v in range(y-size, y+size+1):
            for u in range(x-size, x+size+1):
                if inside(u,v):
                    j = (v*width+u)*4
                    r += pxs0[j]
                    g += pxs0[j+1]
                    b += pxs0[j+2]
                    n += 1
        pxs[i]   = r/n
        pxs[i+1] = g/n
        pxs[i+2] = b/n
        pxs[i+3] = 1.0

blimg.pixels = pxs
```

![image](https://qiita-image-store.s3.amazonaws.com/0/152179/1b78289c-64c5-45ae-ef25-6db23018f6a3.png)


処理効率を一切無視したコードなので実用には向かない(実際遅い)ですが、これで他のプログラミング言語でのピクセル操作とほぼ同じ書き方が行えるようになりました。


### Pythonから画像を別名で出力する

今までは入力と同じBlender内画像に対して出力を行っていましたが、通常は元画像に処理を加えた結果は別画像として出力されるのが習わしです。そこでPythonから新たにBlender内に画像を作成しつつそれにピクセルをセットすることで出力画像とします。

`bpy.data.images.new()`を呼び出すことでBlender内に新たに画像を作成できます。次のコードは名前が`BPY Output`であり白色の新規画像を作成します。

```python
import bpy
imagename = 'BPY Output'
width = 32
height = 32
blimg = bpy.data.images.new(imagename, width, height, alpha=True)
blimg.pixels = [1.0]*(width*height*4)
```

![outputpixels-1](https://qiita-image-store.s3.amazonaws.com/0/152179/cfeb1652-9162-f026-954b-c32f15926a36.png)


`images.new()`の引数はそれぞれ`新規画像名, 横幅, 縦幅, アルファチャンネルの使用の有無`となっています。無用な混乱を避けるためにアルファチャンネルは今のところ使用する設定にしておくことにします。新たにBlender内画像を作成した後、`.pixels`にピクセル情報を代入しています。これで画像を入力とは別に出力することができるようになりました。



### 「Blender Pythonで画像の操作を行う」 まとめ
* `bpy.data.images[<画像名>]`でBlender内画像にアクセスできる
* `blimg.pixels`でその画像のピクセル配列の取得/セットができる
* `bpy.data.images.new()`で新たなBlender内画像が作成できる


<font color="White">.</font>
<font color="White">.</font>



## NumPyを使う

少なくとも現時点で最新のBlender(v2.78)以降ならば、Blender Pythonの中で標準でNumPyが使えるようになっています。

```python
import numpy as np
np.arange(10)     # => array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.arange(10) * 2 # => array([0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
```

NumPyを使うことで高速かつ簡単に配列の操作が行えます。そこでBlender画像のピクセルをNumPyを通して処理してみることにします。



### ピクセル配列 → numpy arrayへの変換

変換といってもピクセル配列を`np.array()`でラップするだけです：

```python
import bpy
import numpy as np

blimg = bpy.data.images['A']
arr = np.array(blimg.pixels[:])
```

これで`pixels`の内容が`numpy array`オブジェクトである`arr`へ変換され、NumPyの機能が適用できるようになりました。


### NumPyの記法を活用する

NumPyの記法を用いると、例えば画像のR成分だけ0にセットするという処理が次のように書けます。(※ 入力画像名=`A`，出力画像名=`B`)

```python
import bpy
import numpy as np

blimg = bpy.data.images['A']
width, height = blimg.size
arr = np.array(blimg.pixels[:])

arr[0::4] = 0.0  # R成分に一括で0を代入

blimg2 = bpy.data.images.new('B', width, height, alpha=True)
blimg2.pixels = arr
```

![numpy-1](https://qiita-image-store.s3.amazonaws.com/0/152179/9be2d479-08bd-ab0c-103f-436fd2596838.png)


ピクセル配列のうち0から始まり4つ飛ばしの各値、すなわちR成分だけを示すために`arr[0::4]`という書き方が使用されています。対象範囲の取得だけなら元のPythonの配列でも同じ書き方ができますが、同じ記法を拡張して代入や操作も可能となっているのがNumPyの特徴ですね。

また次に挙げていくような書き方もできます。NumPyに関しては解説をされているサイトが無数にあるので、詳細に取り上げていくことはここでは控えます。

```python
arr[0::4] *= 0.5  # R値を一括で0.5倍する
arr[1::4] *= 2.0  # G値を一括で2.0倍する
```

```python
# R値を同ピクセルのG値で上書きする
arr[0::4] = arr[1::4]
```

```python
# R値として同ピクセルのG値とB値の平均値を用いる
arr[0::4] = (arr[1::4] + arr[2::4])/2
```

```python
# グレイスケールに変換
gray = 0.299 * arr[0::4] + 0.587 * arr[1::4] + 0.114 * arr[2::4]
arr[0::4] = gray
arr[1::4] = gray
arr[2::4] = gray
```

```python
arr # => array([0, .25, .5, 1.0])

# 浮動小数点数のピクセルを0～255の範囲の整数に変換する
b = (arr*255).astype(np.int)
b[b<0] = 0
b[b>255] = 255
b # => array([0, 63, 127, 255])

# 0～255の範囲の整数から0.0～1.0の浮動小数点数に変換する
c = b.astype(np.float)/255.0
c # => array([0.0, 0.247, 0.498, 1.0])
```


### ピクセルを二次元配列と見なす

`numpy array`は実体はそのままに1次元配列を2次元配列(行列)に見立ててアクセスするということも可能です。例えばRGBAピクセル配列には次のような下処理をしておけば、R成分のみについて `a_R[y][x]` という具合に直感的な形でアクセスすることができるようになります：

```python
import bpy
import numpy as np

blimg = bpy.data.images['A']
W, H = blimg.size

a = np.array(blimg.pixels[:])
b = np.ndarray(len(a))
a.resize(H, W*4)
b.resize(H, W*4)

a_R = a[::, 0::4]
a_G = a[::, 1::4]
a_B = a[::, 2::4]
a_A = a[::, 3::4]

b_R = b[::, 0::4]
b_G = b[::, 1::4]
b_B = b[::, 2::4]
b_A = b[::, 3::4]

for y in range(H):
    for x in range(W):
        b_R[y][x] = a_G[y][x]
        b_G[y][x] = a_B[y][x]
        b_B[y][x] = a_R[y][x]
        b_A[y][x] = a_A[y][x]

b = b.flatten()

blimg2 = bpy.data.images.new('B', W, H, alpha=True)
blimg2.pixels = b
```

上のコードでは元の画像のR,G,BをG,B,Rに置換しているだけですが、既にやったようにインデックス値を`(y*width+x)*4`としてアクセスするより分かりやすい形でピクセルの参照ができています。最後の`b = b.flatten()`では2次元配列から再び1次元に戻しています。



### Box Blur 再び

既に書いたBox Blurを、アルゴリズムそのものは愚直なままNumPyを用いた形に書き直してみます。
例によって簡単のため画像のアルファ値は全て`1.0`であるとします。

```python
# <!> 簡単のために画像のアルファ値は常に1.0とする

import bpy
import numpy as np

blimg = bpy.data.images['A']
W, H = blimg.size

a = np.array(blimg.pixels[:])
b = np.ndarray(len(a))
a.resize(H, W*4)
b.resize(H, W*4)

a_R = a[::, 0::4]
a_G = a[::, 1::4]
a_B = a[::, 2::4]
b_R = b[::, 0::4]
b_G = b[::, 1::4]
b_B = b[::, 2::4]

size = 5
for y in range(H):
    y0 = max(0, y-size)
    y1 = min(H-1, y+size)
    for x in range(W):
        x0 = max(0, x-size)
        x1 = min(W-1, x+size)
        n = (y1-y0)*(x1-x0)
        b_R[y][x] = np.ndarray.sum(a_R[y0:y1, x0:x1]) / n
        b_G[y][x] = np.ndarray.sum(a_G[y0:y1, x0:x1]) / n
        b_B[y][x] = np.ndarray.sum(a_B[y0:y1, x0:x1]) / n

b[::, 3::4] = 1.0 # Alpha == 1.0
b = b.flatten()

blimg2 = bpy.data.images.new('B', W, H, alpha=True)
blimg2.pixels = b
```

![numpy-boxblur](https://qiita-image-store.s3.amazonaws.com/0/152179/ffec2e22-7dfd-8909-c564-442ebb874d26.png)

依然実用には届かないものの、以前のバージョンよりは数倍は高速になりました。



### 「NumPyを使う」 まとめ
* Blenderでは標準でNumPyがインストールされている
* ピクセル配列を`numpy array`の形式に変換することでより簡潔に処理を記述することができる
* 基本的に素のPythonで記述するよりNumPyの機能を活用したほうが高速に動作する


<font color="White">.</font>
<font color="White">.</font>



## Pillow (PIL) を使う

画像処理ライブラリである`Pillow`をインストールし、Blender上で使ってみます。

Pillowは元々はPythonで広く知られている(らしい)`PIL`という画像処理ライブラリのフォーク版ですが、PILの方は更新が停滞しておりBlenderが使用しているPython **3** では使うことが出来ません。よって使用にあたってはPillowの方をインストールする必要があります。

しかしながらインターフェースは(基本的には)両者間で共通のものになっているため、Pillowを利用するにしてもコードやドキュメントはPILのものも活用できると思います。


### Blender PythonにPillowをインストールする (Windowsの場合)

Blender上で動作するPythonに、`pip`を通して外部ライブラリである`Pillow`をインストールする手順を記します。
<font color="DarkGray">※下記の手順はOSがWindowsの場合のみの対応になりますが、他OSでもおおよそ同じ手順になるものと思われます。</font>


PillowのインストールをPythonのパッケージ管理システムである`pip`を通して行います。
通常の環境の(BlenderでなくOSにインストールするタイプの)Pythonならば、Pythonのインストールと同時にpipも一緒にインストールされていることが多いと思いますが、BlenderのPythonはその通りではありません。なので一旦手動でpipをインストールし、そのpipを通して外部ライブラリであるPillowをインストールするという運びになります。


1. https://pip.pypa.io/en/latest/installing/ にアクセスし、`get-pip.py`をダウンロードして適当な場所に置きます。ここでは`D:/temp/get-pip.py`に配置したものとします
2. Windows コマンドプロンプトを開き、BlenderのPythonがあるディレクトリに移動します。Blender Pythonのディレクトリは例えばBlenderが`D:/blender`にインストールされているとすれば、`D:/blender/<Blenderのバージョン>/python`となります。(Blenderのバージョンが2.78なら`D:/blender/2.78/python`)  
   コマンドプロンプトでは  
   `> D:`  
   `> cd D:\blender\2.78\python`  
   と入力すればいいです。
3. 先程ダウンロードした`get-pip.py`を実行して当該ディレクトリに存在するPythonにpipをインストールします。  
   `> bin\python.exe D:\temp\get-pip.py`  
   成功するとカレントディレクトリに`Scripts`ディレクトリが作成され、その中に`pip.exe`が配置されます。
4. pipを通してPillowをインストールします。  
   `> Scripts\pip.exe install pillow`

問題なく手順を進めることができたら、Blenderを起動してスクリプトコンソールで`import PIL`と入力してエラーが生じないことを確かめてください。(`import Pillow`じゃないことに注意。) 何事もなくロードが行えたらPillowのインストールは完了です。

<font color="DarkGray">**※** 環境依存の要因でインストールが上手くいかない場合があるようです。その際は`pip`でなく`easy_install`を通してのインストールを行うと上手くゆく場合もあるみたいです。 </font> [^1] [^2] 




### Blender Python上でPillowを呼び出す

#### :black_small_square: Blender内画像とPillow (PIL) Imageとの相互変換

Blenderの画像データをPillow上の画像データに変換し、Pillow側でフィルタ処理を施した後、再びBlender上の画像データに変換するという流れでPillowを活用してみます。

この相互変換の方法は大きく分けて2通りあります。1つはBlenderのピクセルとPillowのバイト列とを自前で変換する方法と、もう1つはBlender, Pillowのファイルのセーブ/ロード機能を利用して一時ファイルを通しつつ変換する方法です。ここでは後者の一時ファイルを通す方法を採用することにします。

下記の例では入力となるBlender画像データをPillow画像データ(`pimg`)に変換し、それにフィルタ処理を施した結果(`pimg2`)を再びBlender画像データとして出力しています：

##### :white_small_square:  一時ファイルを通して変換を行う

```python
import bpy
import numpy as np
from PIL import Image, ImageFilter

def save_as_png(img, path):
    s = bpy.context.scene.render.image_settings
    prev, prev2 = s.file_format, s.color_mode
    s.file_format, s.color_mode = 'PNG', 'RGBA'
    img.save_render(path)
    s.file_format, s.color_mode = prev, prev2

blimg = bpy.data.images['A']
W,H = blimg.size

temppath = 'd:/temp/bpytemp.png'
# 一時ファイルに保存(Blender)
save_as_png(blimg, temppath)

# 一時ファイルから読み込み(PIL)
pimg = Image.open(temppath)
# PILのフィルタを適用する(ガウシアンブラー)
pimg2 = pimg.filter(ImageFilter.GaussianBlur(radius=5))
# 一時ファイルに保存(PIL)
pimg2.save(temppath)

# 一時ファイルから読み込み(Blender)
blimg2 = bpy.data.images.load(temppath)
blimg2.name = 'B'
```

![pillow-filtered](https://qiita-image-store.s3.amazonaws.com/0/152179/6a31a55f-63b4-1633-2e54-f6bbd74436bf.png)

BlenderからPillowのフィルタを利用できています。



### Pillowのフィルタを触ってみる

既に使用したガウシアンブラーだけでなく様々なフィルタがPillowには用意されています。そこで早速いくつか試してみることにしましょう。
下記のコードは上記のコードのうち`pimg2 = ...`の箇所に置換してから実行してください。

```python:ブライトネスを調整する
from PIL import ImageEnhance
pimg2 = ImageEnhance.Brightness(pimg).enhance( 1.8 )
```
![pillow-filter1](https://qiita-image-store.s3.amazonaws.com/0/152179/79c8f3da-b7b7-f623-8b55-1b8b4d6f6a85.png)

<font color="White">.</font>

```python:反時計回りに回転する
pimg2 = pimg.transpose(Image.ROTATE_90)
```
![pillow-filter2](https://qiita-image-store.s3.amazonaws.com/0/152179/0975a31a-5173-5a4d-99c4-649232941008.png)

<font color="White">.</font>

```python:回転、ブライトネス調整、元の画像を貼り付け、テキストを入力
from PIL import ImageEnhance, ImageDraw, ImageFont
pimg2 = pimg.transpose(Image.ROTATE_90)
pimg2 = ImageEnhance.Brightness(pimg2).enhance( 0.5 )
pimg2.paste(pimg, (70, 70))
draw = ImageDraw.Draw(pimg2)
font = ImageFont.truetype('arial.ttf', 20)
draw.text((20, 5), 'hogehoge', font=font, fill='#88ff88')
```
![pillow-filter3](https://qiita-image-store.s3.amazonaws.com/0/152179/b8dea1b2-5a05-5d2c-d3d3-1b784c0151a0.png)


### 「Pillow (PIL) を使う」 まとめ
* Blender Pythonに外部ライブラリを手動でインストールすることができる
* Pillowのフィルタを使用するためにBlenderとPillowとで画像データを相互に変換する必要がある


<font color="White">.</font>
<font color="White">.</font>







## まとめ

駆け足になりましたが、Blender内部の画像データをPythonで制御する基本的な方法、またNumPyやPillowといった外部ライブラリをBlender Python上で活用する方法についてを、ごく初歩的な内容に留まりながらも解説してきました。ここまでくればBlenderでない通常のPythonについての他のサイトの記事も応用してBlender上で動作させられる筈です。

特にPythonはそのままでは非常に低速で、画像処理のような膨大なイテレーションを必要とするプログラムのパフォーマンスを改善するには NumPy に代表される外部ライブラリについての習熟が必須となるでしょう。今回挙げたものの他にも優れた画像処理用ライブラリはいくつかあるので、機能の不足を感じたり、より速度の改善を望むような場合はそれらをあたってみるのもいいかもしれませんね。

<font color="White">.</font>
<font color="White">.</font>

---

以降は画像のピクセル操作をする際のBlender特有の引っ掛かりどころや、筆者自身の備忘録を含む細かい内容となります。( おそらく随時更新 )


## その他細かい部分についてのTips

### 画像が消えないために

Pythonから画像を新規に作成したり、既存の画像に処理を行ったりすることで画像に何らかの変更を加えたとします。この状態で現在開いているBlenderファイルを保存した後、一旦ファイルを閉じて再び開き直すと、`.blend`ファイルはきちんと保存した筈であるにも関わらずその画像への変更が失われてしまいます。

これはPython関係なくBlenderで作業する上で全般的に注意を要する事項となるのですが、基本的にBlenderは.blendファイルの中で使用する画像への変更を自動的には保存してくれません。(保存されるのは画像の*使用状態*のみ。) 画像へ変更を加えた場合はその保存動作をユーザー自身が行う必要があります。

#### `Save As Image`を使う

UV/Image Editorの`Image`メニューから`Save As Image`を選択して外部に画像を保存できます。その後Blenderファイルの保存を行うことで、先程保存した外部画像への参照が`.blend`ファイルに含まれる形になります。改めてBlenderから先の画像へ変更を加えたら、UV/Image Editorの`Save Image`を選択することで外部画像へ変更が上書き保存されます。

#### `Pack As PNG`を使う

UV/Image Editorの`Image`メニューから`Pack As PNG`を選択した後でBlenderファイルの保存を行うと、その画像が`.blend`ファイルの内部へ埋め込まれる形で保存されます。この操作によって保存した画像に何らかの変更を加えた場合は、再び Pack As PNG → Blenderファイル上書き保存の手順を行うことで画像への変更を上書き保存することが出来ます。(`Save Image`でないことに注意)

#### Python側で保存処理を行う

Pythonから直接外部画像として保存するというのも一つの手です。[以前に定義した](#white_small_square--%E4%B8%80%E6%99%82%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E9%80%9A%E3%81%97%E3%81%A6%E5%A4%89%E6%8F%9B%E3%82%92%E8%A1%8C%E3%81%86)関数`save_as_png()`を利用して、例えば

```python
blimg0 = ... # 保存する画像
path = "d:/temp/abc.png" # 適当なパス
save_as_png(blimg0, path)
blimg = bpy.data.images.load(path)
blimg.name = '適当な画像名'
```

のようにすれば、指定したパスに画像が出力された後で現在のBlenderファイルがその画像を開いているという形となるため、後は.blendファイルの保存のみ行えばいいことになります。
この方法を使った場合における.blendファイル内でのその画像の位置付けは*`Save As Image`のケース*を通したものと同じものとなります。

---

### 画像が消えないために その2

#### 0ユーザー問題

オブジェクトやそのメッシュ、マテリアル、テクスチャ、開いている画像等ひとつのBlenderファイルが使用する各種データ(リソース)には`ユーザー`という概念があり、端的に言えばそのリソースがそのBlenderファイル内でどの程度他から利用されているかを示すものです。

![fakeuser-1](https://qiita-image-store.s3.amazonaws.com/0/152179/216c8d79-7193-5bd1-0a0e-d6eb6fc3c695.png)


特にどこからも利用されていないリソースは`0ユーザー`として認識され、この状態でBlenderファイルを保存・再び開き直すという手順を経ると、0ユーザーだったリソースが自動的に削除されます。

![fakeuser-2](https://qiita-image-store.s3.amazonaws.com/0/152179/2db7f69b-6090-6fc3-779b-24d4a882a832.png)


この仕様は*不要なリソースを保持せず常に最小限のデータ構造を維持できる*という点で有用ですが、気を付けないと*実験的に作成してみたがすぐには使わない*状態にあるリソースまでもが消えてしまいます。とりわけ、この記事で行っているようなPythonから直接新規に作成した画像データはまさしくそれに該当してしまっています。

#### Fake Userを使う

といっても0ユーザー状態を回避するのは実に簡単です。`Fake User`状態にすれば良いだけです。Blenderでの各種データブロックのデータ名の横に`F`マークのボタンがありますが、それをONにすればFake User状態となります。Fake User状態となったデータは常にそのユーザー数が実際よりも`+1`された状態となるため、0ユーザーにならずに済むことになります。

![fakeuser-3](https://qiita-image-store.s3.amazonaws.com/0/152179/4aaa6c46-03e6-758c-f1a5-e61122ca8b16.png)


PythonからFake UserをONにするには、そのデータの`.use_fake_user`に`True`を代入してやればいいです。

```python
blimg = bpy.data.images['A']
blimg.use_fake_user = True
```




---

### レンダリング結果の画像のピクセルにアクセスする際の注意点

#### :black_small_square: `Render Result`のピクセルにアクセスできない

レンダリング結果の画像は`UV/Image Editor`の中の`Render Result`に格納されますが、Pythonでこの画像にアクセスしても何故かサイズが常に`0`を返され、またピクセル配列も空となっておりそのままではアクセスできません。

```python
import bpy
blimg = bpy.data.images['Render Result']
width, height = blimg.size
print(width, height) # => 0, 0
print(len(blimg.pixels)) # => 0
```

この問題を回避するいくつかの方法があります。

##### :white_small_square: `Viewer Node`を使用する

`Node Editor`を開きタイプを`Compositing`に切り替え、コンポジターを開きます。

![userenderresult-compbutton](https://qiita-image-store.s3.amazonaws.com/0/152179/82bd126f-d1f6-1753-ac81-bbbaf57efca8.png)

`Use Nodes`をONにし、`Add > Output > Viewer`から`Viewer`ノードを追加、レンダリング結果に繋げます。

![userenderresult-connectviewer](https://qiita-image-store.s3.amazonaws.com/0/152179/f844f89d-18ae-08b1-e200-4cfbb843ed13.png)



すると UV/Image Editor の中に`Viewer Node`という名前の画像が作成され、またその内容は`Render Result`と同じです。このViewer NodeにPythonからアクセスすれば当該画像のピクセルにアクセスできます。

```python
import bpy
blimg = bpy.data.images['Viewer Node']
width, height = blimg.size
pxs = blimg.pixels[:]

blimg2 = bpy.data.images.new('B', width, height, alpha=True)
blimg2.pixels = pxs
```

###### :white_small_square: カラーマネジメントにより色が変わる

しかしながら上のコードを実行してみると、単に元の画像をそのまま別画像にコピーしただけのものでありながら、両者間で色味が違ってしまいます。

![userenderresult-differs](https://qiita-image-store.s3.amazonaws.com/0/152179/ef4598a3-f703-47fa-439d-18973dcd0b11.png)

何が起こっているかというと、Blenderは特定の画像に対して`カラーマネジメント`による補正を自動的にかける仕組みになっていて、特に今回のようなレンダリング結果の画像がそれに該当します。

今回のケースではBlenderのカラーマネジメントシステムにより元のピクセルに暗黙的にガンマ補正が掛けられているものが最終的に表示されている<font color="DarkGray">(※2)</font>ため、その結果を明示的に得るために自前で同じ補正を掛けてやればよさそうです：

```python
import bpy
import numpy as np

def fix_colormanagement(a):
    a[0::4] = np.power(a[0::4]/a[3::4], 1/2.2)
    a[1::4] = np.power(a[1::4]/a[3::4], 1/2.2)
    a[2::4] = np.power(a[2::4]/a[3::4], 1/2.2)

blimg = bpy.data.images['Viewer Node']
width, height = blimg.size
pxs = blimg.pixels[:]

a = np.array(pxs)
fix_colormanagement(a)
        
blimg2 = bpy.data.images.new('B', width, height, alpha=True)
blimg2.pixels = a
```

![userenderresult-fixed](https://qiita-image-store.s3.amazonaws.com/0/152179/9f143f12-f76b-60ed-7f97-454aa84575ab.png)

うまいこと補正されました。

<font color="DarkGray">※　ここで補正のためのガンマ値を`2.2`に決め打ちしてしまいましたが、もしかしたら環境によっては異なる値を使用しないといけないかもしれません。</font>
<font color="DarkGray">※2　ここでの処理はカラーマネジメントの設定がデフォルトの時のみの対応となってます。</font>


###### :white_small_square: カラーマネジメントを無効にする

上記のやり方とは別に、Blenderのカラーマネジメントシステムそのものを無効にしてしまうという手もあります。

![userenderresult-propscene](https://qiita-image-store.s3.amazonaws.com/0/152179/b9d19719-489e-0004-cdb9-3fd2b9ebb344.png)

プロパティの`Scene`タブから`Color Management`パネルを開き、その中の`Display Device:`を`None`にする、もしくは`View:`を`Raw`に変更することでこれを無効にできるようです。

![userenderresult-cmoff](https://qiita-image-store.s3.amazonaws.com/0/152179/0c27dd16-f10e-1971-82e3-d6da3e8b4465.png)

Viewer Node画像の補正がOFFになりました。



#### :black_small_square: 一旦ファイルに保存して回避する

レンダリング結果を一旦ファイルとして保存しておき、それをロードすることで上述した各種の問題を回避できます。保存した画像の結果はカラーマネジメントが適用済みのものになります。
また`Render Result`そのままでよく、`Viewer Node`を通す必要もありません。

```python
import bpy
blimg = bpy.data.images['Render Result']
temppath = 'd:/temp/bpytemp.png'
save_as_png(blimg, temppath)

blimg2 = bpy.data.images.load(temppath)
blimg2.name = 'B'
print(len(blimg2.pixels))
```

( 関数`save_as_png()`は[Blender Python上でPillowを呼び出す > 一時ファイルを通して変換を行う](#white_small_square--%E4%B8%80%E6%99%82%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E9%80%9A%E3%81%97%E3%81%A6%E5%A4%89%E6%8F%9B%E3%82%92%E8%A1%8C%E3%81%86)のときと同じ )

---

### 画像出力を少し楽にするユーティリティ

Blender Pythonから画像の出力を行うには、既に書いたように次のようにすればいいのでした：

```python
imagename = 'New Image'
width = 32
height = 32
pxs = [1.0]*(width*height*4)

blimg = bpy.data.images.new(imagename, width, height, alpha=True)
blimg.pixels = pxs
```

上記のやり方でも十分といえば十分なのですが、同名の画像を連続で生成すると、重複を避けるためにBlenderが自動的にサフィックスを付けた別名の画像、例えば`New Image.001`という名前の画像が作成されてしまいます。

![makeiteasy-1](https://qiita-image-store.s3.amazonaws.com/0/152179/28a9dd2b-1d66-8e3b-331c-055f161e516d.png)


大抵の場合コードを書き進める過程では試行錯誤のため何度も実行を行うのが常であり、その度に結果を確認するためにUV/Image Editorにて別名に保存された画像を大量の画像の中から選択し直すのは億劫です。そこで、画像を新規作成する際に同名画像があった場合はそれを事前に自動で削除する(もしくは使いまわす)ようにした次のユーティリティ関数 `make_blimage()` を使うといいでしょう：

```python
def make_blimage(width, height, pixels, imgname='Py Output'):
    # 同名の画像が既に存在し、かつ同サイズだった場合にはそれを使いまわし、サイズが異なれば新たに作成し直す
    if imgname in bpy.data.images and bpy.data.images[imgname].size[:] != (width,height):
        older = bpy.data.images[imgname]
        older.user_clear()
        bpy.data.images.remove( older )

    if imgname not in bpy.data.images:
        img = bpy.data.images.new(imgname, width, height, alpha=True)
        img.generated_color = (0,0,0,0)
        img.use_fake_user = True

    img = bpy.data.images[imgname]
    img.pixels = pixels
    return img
```

次のようにして使います。

```python
imagename = 'New Image'
width = 32
height = 32
pxs = [1.0]*(width*height*4)

make_blimage(width, height, pxs, imagename)
```

画像処理の際にはUV/Image Editorを2つ並べて入力と出力画像をそれぞれに表示しておくと便利ですが、上の関数を使えば出力画像が同サイズである限りはスクリプトの実行後にImage Editorで出力画像を開き直す手間が省けるので楽ができます。

![makeiteasy-2](https://qiita-image-store.s3.amazonaws.com/0/152179/df6eeecd-c955-4bf3-3841-2a5f82731ea8.jpeg)



---


### NumPy: プリマルチプライド・アルファ ←→ ストレート・アルファ変換

アルファチャンネルの有効な画像の処理でたびたび必要になるプリマルチプライド・アルファ(Premultiplied Alpha)とストレート・アルファ(Straight Alpha)との変換ですが、NumPyを使えば簡単に行えます。

```python
a = np.array(blimg.pixels[:])
a_R = a[0::4]
a_G = a[1::4]
a_B = a[2::4]
a_A = a[3::4]

## straight to premul alpha
a_R *= a_A
a_G *= a_A
a_B *= a_A

a # => プリマルチプライド・アルファに変換されたカラー

## premul to straight alpha
nonzero_alpha = a_A != 0
a_A_nonzero = a_A[nonzero_alpha]
a_R[nonzero_alpha] /= a_A_nonzero
a_G[nonzero_alpha] /= a_A_nonzero
a_B[nonzero_alpha] /= a_A_nonzero

a # => ストレート・アルファに変換されたカラー
```

ここでアルファ値は既に`0.0～1.0`の範囲に収まっていることに注意。
元のストレートアルファなカラーについて、アルファ値を乗算すればプリマルチプライドに、除算すればストレートになるというだけの話ですが、除算の際は`0`で割ることはできないため、アルファ値が0の場合は処理を除外するために`nonzero_alpha`に見られるNumPyのインデックス記法を活用しています。

プリマルチプライド/ストレート・アルファの変換が必要なケースとして、例えば半透明な画像同士の線形補間(クロスフェード)があります。線形補間自体は非常に単純で、 $t$ が0.0～1.0で2つの画像の画素値が $a, b$ とすると、その線形補間は $c = a(1-t)+bt$ です。

```python
def to_premul(a):
    a_R = a[0::4]
    a_G = a[1::4]
    a_B = a[2::4]
    a_A = a[3::4]

    a_R *= a_A
    a_G *= a_A
    a_B *= a_A

def to_straight(a):
    a_R = a[0::4]
    a_G = a[1::4]
    a_B = a[2::4]
    a_A = a[3::4]

    nonzero_alpha = a_A != 0
    a_A_nonzero = a_A[nonzero_alpha]
    a_R[nonzero_alpha] /= a_A_nonzero
    a_G[nonzero_alpha] /= a_A_nonzero
    a_B[nonzero_alpha] /= a_A_nonzero

blimg1 = bpy.data.images['img1']
blimg2 = bpy.data.images['img2']
assert blimg1.size[:] == blimg2.size[:]

a = np.array(blimg1.pixels[:])
b = np.array(blimg2.pixels[:])

to_premul(a)
to_premul(b)
t = 0.5
c = a*(1-t)+b*t
to_straight(c)

W, H = blimg1.size
blimg_out = bpy.data.images.new('B', W, H, alpha=True)
blimg_out.pixels = c
```

上のコードでは事前に`to_premul()`で2つの画像のピクセルをプリマルチプライド・アルファのカラーに変換しておき、それらの間で線形補間を行った結果である`c`を最終的に`to_straight()`でストレート・アルファに変換しなおしています。
`t`を`0.0`から`1.0`まで動かした結果と、もし`to_premul()`, `to_straight()`の呼び出しが行われなかったらどうなるかの結果も出力したのが下のアニメーションになります：

![crossfade](https://qiita-image-store.s3.amazonaws.com/0/152179/77fbafe0-9904-f074-b9f4-64dcb8eec359.gif)

左がプリマルチプライド/ストレート・アルファ変換を行わなかった場合の結果で、右がきちんと行ったときの結果です。見ての通り左では補間の中間で結果に不備が生じています。片方の画像のアルファ値が0であるものの実際には残っているRGBのカラー情報が漏れ出している形ですね。


---

### Pillow: Blender画像のピクセルを直接Pillow画像のバイト列に変換する

Pillowの項で軽く触れた「BlenderのピクセルとPillowのバイト列とを自前で変換する方法」についてここに記しておきます。諸々の理由であまり実用的ではありません。(後述)

```python
import bpy
import numpy as np
from PIL import Image, ImageFilter

blimg = bpy.data.images['A']
W,H = blimg.size
px0 = blimg.pixels[:]

# Blender用ピクセルを0～255の整数に変換
a = (np.array(px0)*255).astype(np.int)
a[a<0] = 0
a[a>255] = 255
a = a.astype(np.uint8)

# ピクセルをPIL Imageに変換
import array
pimg = Image.frombytes("RGBA", (W,H), array.array("B", a).tostring() )
# PILのフィルタを適用する(ガウシアンブラー)
pimg2 = pimg.filter(ImageFilter.GaussianBlur(radius=5))
W2,H2 = pimg2.size

# PIL ImageをBlender用ピクセルに変換
pil_bytes2 = np.asarray( pimg2 ).flatten().flatten()
pxs = pil_bytes2 / 255.0

blimg2 = bpy.data.images.new('B', W2, H2, alpha=True)
blimg2.pixels = pxs
```

**(注意)** BlenderとPillowとで画像データをやりとりする上での既に挙げた異なる2つの方式のうち、基本的には`一時ファイルを通す`方式の方を使ったほうが無難です。Blenderの画像のピクセルデータは画像の左下から始まるのに対し、Pillowの画像は左上から始まるため、ピクセルを直接変換する方式ではPillowのフィルタによっては結果に不備が生じます。(自前で上下反転させるというのも余分なコストになります。)
また大きな画像を扱う際にもファイルを通したほうがパフォーマンスに優れているかもしれません。

<font color="White">.</font>
<font color="White">.</font>

---

## 参考


* Installation — Pillow (PIL Fork) 3.1.2 documentation
  http://pillow.readthedocs.io/en/3.1.x/installation.html

* Python 3.5 対応画像処理ライブラリ Pillow (PIL) の使い方 - Librabuch 
  https://librabuch.jp/blog/2013/05/python_pillow_pil/

* コンポジターに必要なアルファチャンネルの知識（後編） - コンポジゴク
  http://compojigoku.blog.fc2.com/blog-entry-5.html

---

[^1]: Installation — Pillow (PIL Fork) 3.1.2 documentation
  http://pillow.readthedocs.io/en/3.1.x/installation.html

[^2]: Python 3.5 対応画像処理ライブラリ Pillow (PIL) の使い方 - Librabuch 
  https://librabuch.jp/blog/2013/05/python_pillow_pil/

<font color="White">.</font>
<font color="White">.</font>

---
> この記事は [Blender Advent Calendar](http://www.adventar.org/calendars/1571) 12/08 分の記事でした。
明日 (12/09) の記事は psi_ni_phi さんの「キャラクターデザインのちょっとしたTIPSとメイキングを載せます」になります。