夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

BlenderでGIF書き出しプラグインを作る その④ GIFの生成設定

本日はPython枠です。

先日BlenderでGIFの作成に関して行いました。

今回はGIFの生成設定を実装していきます。

〇GIFフォーマットの設定

GIF(Graphics Interchange Format)は簡易アニメーションを実現できる動く画像です。

GIF画像はフレーム処理時間、ループ回数などの設定を行うことができます。

一般的にループ回数に関しては無限ループを期待されているためいじることはないですが、フレーム処理時間に関してはGIFアニメーションの速度になるので変更できると便利です。

今回は作成したGIFエクスポーターでこのフレーム処理時間を変数としてユーザーが任意にいじれるようにしていきます。

 また、GIF画像全体のアニメーションの時間はフレーム数×フレーム処理時間で求めることができるため、このデータを使用してアニメーションの時間もユーザーがいじれるようにしていきます。

〇GIF生成のスクリプトの修正

現在のGIF生成スクリプトのコードはこちらになります。

GIF_Creator.py

import os
import imageio
import re
from PIL import Image, ImageSequence

def create_gif(directory, output_filename):
    # ディレクトリ内のすべてのファイル名を取得
    filenames = os.listdir(directory)

    # 画像ファイルのみをフィルタリング
    image_filenames = [os.path.join(directory, fn) for fn in filenames if fn.endswith('.png') or fn.endswith('.jpg')]

    # 数字を含む部分を抽出してソート
    image_filenames.sort(key=lambda x: int(re.search(r'\d+', x).group()))
    # 画像数を出力
    print(len(image_filenames))

    # GIFを作成
    images = []
    for filename in image_filenames:
        print(filename)
        image = Image.open(filename)
        images.append(image)

    images[0].save(
        output_filename,
        save_all=True,
        append_images=images[1:],
        duration=[1/24] * len(images),  # すべてのフレームに同じdurationを指定
        loop=0
    )

def check_gif():
    # GIFファイルを開く
    gif_image = Image.open(r'C:\Users\seiri\Documents\PythonStudy\GIFCreator\output\output.gif')

    # フレーム数と各フレームの表示時間を取得
    metadata = []
    for i in range(gif_image.n_frames):
        gif_image.seek(i)
        duration = gif_image.info.get("duration", 0)
        metadata.append(duration)

    # 結果を表示
    print(f"フレーム数: {gif_image.n_frames}")
    print(f"各フレームの表示時間(ミリ秒): {metadata}")

input_path = r'C:\tmp'
gif_output_path = r'C:\tmp\output.gif'
create_gif(input_path, gif_output_path)
check_gif()

今回の実装ではBlenderPythonスクリプトから上記外部プロジェクトのスクリプトを実行する際にパスなどの書き換えを行っています。

 書き換えはコードの書き換えに相当するため通常は推奨されませんがPythonが動的型付け言語のため可能となっています。

 可能とはいっても予期しないトラブルを回避するために、また可読性を挙げるため今回は変数として定義して、その変数部のみを書き換えるようにしています。

 今回はフレーム処理時間=durationを変更可能にしたいため、現在のdurationを確認します。

    images[0].save(
        output_filename,
        save_all=True,
        append_images=images[1:],
        duration=[1/24] * len(images),  # すべてのフレームに同じdurationを指定
        loop=0
    )

この1/24と入力されているマジックナンバー(笑)を表に出していきます。

input_path = r'C:\tmp'
gif_output_path = r'C:\tmp\output.gif'
duration_num = 1000
create_gif(input_path, gif_output_path)
check_gif()

同様にduration_numを引数として扱えるようにします。

def create_gif(directory, output_filename,duration_num_str):
 ・・・
    duration_num = float(duration_num_str)
    # GIFを作成
    images = []
    for filename in image_filenames:
        print(filename)
        image = Image.open(filename)
        images.append(image)

    images[0].save(
        output_filename,
        save_all=True,
        append_images=images[1:],
        duration=[duration_num] * len(images),  # すべてのフレームに同じdurationを指定
        loop=0
    )

input_path = r'C:\tmp'
gif_output_path = r'C:\tmp\output.gif'
duration_num_str = r'1000'
create_gif(input_path, gif_output_path, duration_num_str)
check_gif()

正直raw文字列を使用しなくてもよいのですがパスに合わせて同じコードを使用したかったためこちらを使用しました。

以上でGIFを作成するスクリプトの改修は完了です。

Blender側でDurationの設定を行う

class GIFExporterPanel(bpy.types.Panel):
    ・・・
    def draw(self, context):
        layout = self.layout
   ・・・
        layout.prop(context.scene, "loop_step_frame")
        layout.label(text="GIF Export Settings")#追加
        layout.prop(context.scene, "gif_duration")#追加
        layout.operator("object.loop_render")
・・・
def register():
        bpy.utils.register_class(GIFExporterOperator)
  ・・・
        bpy.types.Scene.loop_step_frame = bpy.props.IntProperty(name="Step",default =1)
        bpy.types.Scene.gif_duration = bpy.props.IntProperty(name="GIF Duration(/mSec)" ,default = 1)#追加

これによってDurationのパラメータがUI上に出現します。

今回はラベルも追加してUIとして見やすい形にしています。

次にパス同様にdurationを書き換えるように処理をします。

def gif_maker_init():
    gif_maker_file_path = r'C:\Users\seiri\Documents\PythonStudy\GIFCreator\gif_creator.py'
    with open(gif_maker_file_path, 'r') as f:
        content = f.read()    
  
    input_pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    output_pattern = r'(gif_output_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    duration_pattern = r'(duration_num_str\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    input_matches = re.findall(input_pattern, content)
    output_matches = re.findall(output_pattern,content)
    duration_matches = re.findall(duration_pattern,content)
    print(len(duration_matches))
    new_content = content
    for match in input_matches:
        # match[0]は"input_path = r\"", match[1]は"..."(ダブルクォート内のパス), match[2]は"\""
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}C:\{bpy.context.scene.gif_input}{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    for match in output_matches:
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}C:\{bpy.context.scene.gif_output}\output.gif{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    for match in duration_matches:
        old_string =f'{match[0]}{match[1]}'
        new_string =f'{match[0]}{bpy.context.scene.gif_duration}'
        new_content = new_content.replace(old_string, new_string)
        print(new_content)
    with open(gif_maker_file_path, 'w') as f:
        f.write(new_content)
    
    gif_maker_excute(gif_maker_file_path)

これによってdurationを書き換えることができます。

〇UIの微改善 パラメータが増えたので改善します。

class GIFExporterPanel(bpy.types.Panel):
    bl_label = "GIF Exporter"
    bl_idname = "OBJECT_PT_hello_world"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'GIFExporter'

    def draw(self, context):
        layout = self.layout
        layout.prop(context.scene, "gif_input")
        layout.prop(context.scene, "gif_output")
        layout.prop(context.scene, "loop_start_frame") 
        layout.prop(context.scene, "loop_end_frame")
        layout.prop(context.scene, "loop_step_frame")
        layout.label(text="GIF Export Settings")
        layout.prop(context.scene, "use_duration")#追加
        if context.scene.use_duration:
            layout.prop(context.scene, "gif_duration")
        else:
           print("Log")
        layout.label(text="Excute")#追加
        layout.operator("object.loop_render")
・・・

def register():
        ・・・・
        bpy.types.Scene.loop_step_frame = bpy.props.IntProperty(name="Step",default =1)
        bpy.types.Scene.use_duration = bpy.props.BoolProperty(name="Use_Duration",default =True) #追加
        bpy.types.Scene.gif_duration = bpy.props.IntProperty(name="GIF Duration(/mSec)" ,default = 1)

上記改善ではUIにラベルとチェックボックスを表示させDurationを調整したい場合UIが表示されるようにしました。

チェックを入れていない状態ではパラメータが隠れるようになりました。

〇時間指定

 次にdurationを指定しない場合Blenderのアニメーションウィンドウの再生長さと一致させていきます。

現在出力プロパティで設定されているfps取得するには次の処理を使用します。

# 現在のシーンを取得
current_scene = bpy.context.scene

# 現在のシーンのfpsを取得
fps = current_scene.render.fps

print(fps)

step数がnの場合はn/(fsp)×1000のdurationを設定すればBlenderのアニメーション速度と一致します。

これを現状のコードに組み込み、もしもチェックボックスがオフの場合は先ほどの計算結果をdurationの値として使用するようにします。

def gif_maker_init():
    if not bpy.context.scene.use_duration:
        # 現在のシーンを取得
        current_scene = bpy.context.scene

        # 現在のシーンのfpsを取得
        fps = current_scene.render.fps
        bpy.context.scene.gif_duration = int(bpy.context.scene.loop_step_frame * 1000 / fps)


    gif_maker_file_path = r'C:\Users\seiri\Documents\PythonStudy\GIFCreator\gif_creator.py'
    with open(gif_maker_file_path, 'r') as f:
        content = f.read()

    input_pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    output_pattern = r'(gif_output_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    duration_pattern = r'(duration_num_str\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    input_matches = re.findall(input_pattern, content)
    output_matches = re.findall(output_pattern, content)
    duration_matches = re.findall(duration_pattern, content)
    print(len(duration_matches))
    new_content = content
    for match in input_matches:
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}C:\{bpy.context.scene.gif_input}{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    for match in output_matches:
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}C:\{bpy.context.scene.gif_output}\output.gif{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    for match in duration_matches:
        old_string = f'{match[0]}{match[1]}'
        new_string = f'{match[0]}{bpy.context.scene.gif_duration}'
        new_content = new_content.replace(old_string, new_string)
        print(new_content)
    with open(gif_maker_file_path, 'w') as f:
        f.write(new_content)

    gif_maker_excute(gif_maker_file_path)

以上で独自にDurationを設定することも、Blenderのアニメーションと一致させることもできました。

本日は以上です。

〇コード一覧

#アドオンの定義
bl_info = {
    "name": "GIFMaker",
    "blender": (3, 5, 0),
    "category": "Object",
}


import bpy
import os
import re
import subprocess

# Define a new operator (action or function)
class GIFExporterOperator(bpy.types.Operator):
    bl_idname = "object.gif_maker"
    bl_label = "Hello, World!"

    def execute(self, context):
        self.report({'INFO'}, "Hello, World!")
        return {'FINISHED'}


# Define a new UI panel
class GIFExporterPanel(bpy.types.Panel):
    bl_label = "GIF Exporter"
    bl_idname = "OBJECT_PT_hello_world"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'GIFExporter'

    def draw(self, context):
        layout = self.layout
        layout.prop(context.scene, "gif_input")
        layout.prop(context.scene, "gif_output")
        layout.prop(context.scene, "loop_start_frame") 
        layout.prop(context.scene, "loop_end_frame")
        layout.prop(context.scene, "loop_step_frame")
        layout.label(text="GIF Export Settings")
        layout.prop(context.scene, "use_duration")
        if context.scene.use_duration:
            layout.prop(context.scene, "gif_duration")

        layout.label(text="Excute")
        layout.operator("object.loop_render")



        
class LoopRenderOperator(bpy.types.Operator):
    bl_idname = "object.loop_render"
    bl_label = "Loop Render"


    def execute(self, context):
        for frame in range(bpy.context.scene.loop_start_frame, bpy.context.scene.loop_end_frame + 1, bpy.context.scene.loop_step_frame):
            bpy.context.scene.frame_set(frame)
            bpy.context.scene.render.filepath = f"{bpy.context.scene.gif_input}/render_{frame}"
            bpy.ops.render.render(write_still=True)
        gif_maker_init()
        return {'FINISHED'}

def gif_maker_excute(script_path):
    python_path = r"C:\Users\seiri\anaconda3\envs\gif-creator\python.exe" 

    if not os.path.exists(script_path):
        print(f"スクリプトが見つかりません: {script_path}")
    else:
        # subprocessを使用してスクリプトを実行します
        result = subprocess.run([python_path, script_path], capture_output=True, text=True,encoding='utf-8',errors='replace')


def gif_maker_init():
    if not bpy.context.scene.use_duration:
        # 現在のシーンを取得
        current_scene = bpy.context.scene

        # 現在のシーンのfpsを取得
        fps = current_scene.render.fps
        bpy.context.scene.gif_duration = int(bpy.context.scene.loop_step_frame * 1000 / fps)


    gif_maker_file_path = r'C:\Users\seiri\Documents\PythonStudy\GIFCreator\gif_creator.py'
    with open(gif_maker_file_path, 'r') as f:
        content = f.read()

    input_pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    output_pattern = r'(gif_output_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    duration_pattern = r'(duration_num_str\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    input_matches = re.findall(input_pattern, content)
    output_matches = re.findall(output_pattern, content)
    duration_matches = re.findall(duration_pattern, content)
    print(len(duration_matches))
    new_content = content
    for match in input_matches:
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}C:\{bpy.context.scene.gif_input}{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    for match in output_matches:
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}C:\{bpy.context.scene.gif_output}\output.gif{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    for match in duration_matches:
        old_string = f'{match[0]}{match[1]}'
        new_string = f'{match[0]}{bpy.context.scene.gif_duration}'
        new_content = new_content.replace(old_string, new_string)
        print(new_content)
    with open(gif_maker_file_path, 'w') as f:
        f.write(new_content)

    gif_maker_excute(gif_maker_file_path)


        

def register():
        bpy.utils.register_class(GIFExporterOperator)
        bpy.utils.register_class(GIFExporterPanel)
        bpy.utils.register_class(LoopRenderOperator)
        bpy.types.Scene.gif_input = bpy.props.StringProperty(name="Temp Path:",default="tmp")
        bpy.types.Scene.gif_output = bpy.props.StringProperty(name="GIFOutput Path:",default="tmp")
        bpy.types.Scene.loop_start_frame = bpy.props.IntProperty(name="Start Frame", default=1)  # Add this line
        bpy.types.Scene.loop_end_frame = bpy.props.IntProperty(name="End Frame", default =20)
        bpy.types.Scene.loop_step_frame = bpy.props.IntProperty(name="Step",default =1)
        bpy.types.Scene.use_duration = bpy.props.BoolProperty(name="Use_Duration",default =True)
        bpy.types.Scene.gif_duration = bpy.props.IntProperty(name="GIF Duration(/mSec)" ,default = 1)



def unregister():
        bpy.utils.unregister_class(GIFExporterOperator)
        bpy.utils.unregister_class(GIFExporterPanel)
        bpy.utils/unregister_class(LoopRenderOperator)
        del bpy.types.Scene.my_text_input

if __name__ == "__main__":
    register()