Blenderのシーンでマーカーのあるフレームをレンダリングして台紙画像に並べる

アニメーション作品作成で 先に動画コンテのようなラフなシーンを作成して
本制作前に紙のコンテ用紙のようなものに並べたいといった場合もあります。

Blenderはタイムライン上でMキーを押すことで任意の位置にマーカーで目印を浸かられます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/a5f1d300-5b80-4180-a793-e46ab767e6a2.png)
1つのシーン上で このマーカーを打ったフレームの画像をレンダリングして
決まった台紙画像上に並べる処理を作ってみました。

```py:PlaceRenderOnSstoryboard01.py
import bpy
import os
import numpy as np

context = bpy.context
# 台紙画像のファイルパス
base_image_filepath = "//ConteBase01.png"
# 並べる画像の開始位置[横,縦]（左上基準）
offset_postion = [230,280]
# カット画像を並べる間隔
tile_height = 525
# 
save_path = "//"

image_base_name = "conte_"
#動画からコンテを作成
def main(context):
    scene = context.scene
    scene.render.use_compositing
    scene.use_nodes = True
    node_tree = scene.node_tree
    # レンダリングの解像度をカットの縦方向と合わせる
    scene.render.resolution_percentage = tile_height*100 //scene.render.resolution_y 
    # 台紙画像の読み込み
    base_img_array = load_as_nparray(base_image_filepath)
    # 台紙画像の大きさの取得
    base_height ,base_width = base_img_array.shape[:2]
    # レンダリング画像をビューアーに出力するノードを作成
    render_rayer_node = node_tree.nodes.new('CompositorNodeRLayers') 
    viewer_node = node_tree.nodes.new('CompositorNodeViewer') 
    node_tree.links.new(render_rayer_node.outputs[0], viewer_node.inputs[0])
    # カットをレンダリングして合成
    page_cuts = (base_height - offset_postion[1]) // tile_height
    page_count = 0
    for i, marker in enumerate (scene.timeline_markers) :
        # マーカーのフレームに移動
        scene.frame_current = marker.frame
        # レンダリング
        bpy.ops.render.render()
        img_array = img_to_nparray(bpy.data.images['Viewer Node'])
        # 台紙画像への割り当て
        if i//page_cuts +1 > page_count:
            if i != 0:
                # 最初の１枚目でなかった場合　前のページの画像を保存
                #numpyarray_to_img(base_img_array,image_name)
                save_nparray_png( save_path, image_name +".png", base_img_array )
                # 台紙画像の読み込み
                base_img_array = load_as_nparray(base_image_filepath)
            page_count += 1
            image_name = "%s_%02d"  % (image_base_name, page_count)
        # 画像は左下基準
        cut_h = base_height -offset_postion[1] -tile_height*(i % page_cuts)
        base_img_array[cut_h -img_array.shape[0] : cut_h, offset_postion[0]: offset_postion[0] +img_array.shape[1] ] = img_array
    # 画像の保存
    #numpyarray_to_img(base_img_array,image_name)
    save_nparray_png( save_path, image_name +".png", base_img_array )
    # 不要なノードの削除
    node_tree.nodes.remove(render_rayer_node)
    node_tree.nodes.remove(viewer_node)

def img_to_nparray(img):
    """rgbaの画像をnumpy arrayに変換"""
    (width,height) = img.size
    channels = img.channels #色数
    #numpy arrayを作成
    pix_array = np.array(img.pixels)
    pix_array = pix_array.reshape( height, width, channels)
    return( pix_array )

def load_as_nparray(f_path):
    """画像の読み込み"""
    abspath = bpy.path.abspath(f_path)
    img = bpy.data.images.load(abspath)
    img_array = img_to_nparray(img)
    #読み込んだ画像オブジェクトの消去
    bpy.data.images.remove(img)
    return(img_array)

def save_nparray_png( save_path, img_name, np_array ):
    """numpy arrayをPNG画像として保存"""
    abspath = bpy.path.abspath(save_path)
    f_path = os.path.join( abspath, img_name )
    (height, width, deps) = np_array.shape
    #画像オブジェクトの作成
    img = bpy.data.images.new(name=img_name, width=width, height=height)
    img.pixels = list( np_array.flatten())
    #保存
    img.save_render(filepath=f_path)
    #画像オブジェクトの消去
    bpy.data.images.remove(img)

main(context)
```
タイムライン上にマーカーを打った.blendファイルと
ConteBase01.png という名前で下のような画像を同じフォルダに置いて実行してください
![ConteBase01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/19f0054a-5a29-4f93-8370-c65d4dfbd462.png)
設定した間隔で画像を並べた画像ファイルを作成します
（収まらい枚数の場合は次の画像を新たに作成します）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/90c71ed2-b6af-4c08-bf85-e3f77f8f21fc.png)
Blender内で完結させるためにnumpyでの画像合成をしています

:::note warn
尚、Blenderの仕組み上　このスクリプトでは「シーケンサー」を使っている場合には動作しません
:::

台紙にする画像を変更したり　並べる間隔を制御できるようになっています
これをそのまま使うことはないでしょうが　何かの参考になれば幸いです