夜風のMixedReality

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

BlenderでGIF書き出しプラグインを作る その③ 仕上げ

本日はBlender Python枠です。

昨日GIF書き出しを作ったのでUI等を仕上げていきます。

redhologerbera.hatenablog.com

〇現状のコード

現状のコード

#アドオンの定義
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, "my_text_input")
        layout.operator("object.loop_render")
        # Add a text field to the panel



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

    start_frame: bpy.props.IntProperty(name="Start Frame", default=1)
    end_frame: bpy.props.IntProperty(name="End Frame", default=20)
    step: bpy.props.IntProperty(name="Step", default=1)

    def execute(self, context):
        for frame in range(self.start_frame, self.end_frame + 1, self.step):
            bpy.context.scene.frame_set(frame)
            bpy.context.scene.render.filepath = f"{bpy.context.scene.my_text_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():
    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()    

    pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    matches = re.findall(pattern, content)
    
    new_content = content
    for match in 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.my_text_input}{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    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.my_text_input = bpy.props.StringProperty(name="Temp Path:",default="tmp")

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()

〇GIF出力の変更

 現状はBlenderレンダリングしシーケンス画像を作成し、その出力パスをGIF作成スクリプトの入力パスとして書き換えて出力しています。

 そのため次は出力パスをBlender内で任意に指定できるようにしていきます。

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.operator("object.loop_render")


def register():
  ・・・
        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")#追加
        

この変更により新たなテキストフィールドが現れます。

入力値を書き換えたように出力に関するパスも書き換えます。

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*[\'\"])(.*?)([\'\"])'
    input_matches = re.findall(input_pattern, content)
    output_matches = re.findall(output_pattern,content)
    
    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)
    with open(gif_maker_file_path, 'w') as f:
        f.write(new_content)
    
    gif_maker_excute(gif_maker_file_path)

このように実装することで入力、出力のパスを任意に書き換えることができました。

〇[バグ修正]画像の読み込み順の確認

現状の問題点としてはGIF画像を作成する際にファイルの名前順で読み込んでいます。

20個のファイルでログを出してみると次のように結果が表示されます。

見てわかるようにこれでは連番にならないため期待しているシーケンス画像のGIF画像にはならないことがわかります。

これを改善するために画像のロード時にソートを行います。

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())) #追加

これによって順番通りにロードされるようになります。

レンダリング設定の実装

 現在のコードでは埋め込みの内部定数としてレンダリングのフレームを指定していました。

    start_frame: bpy.props.IntProperty(name="Start Frame", default=1)
    end_frame: bpy.props.IntProperty(name="End Frame", default=20)
    step: bpy.props.IntProperty(name="Step", default=1)

このパラメータをUI上に出しましょう

class GIFExporterPanel(bpy.types.Panel):
    ・・・
    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.operator("object.loop_render")


def register():
  ・・・
        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)

これによって開始フレーム、終了フレーム、ステップそれぞれのパラメータが表示されます。

最後にここで定義したパラメータを内部で使用するようにします。

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'}

 これによって指定したパラメータを使用してレンダリングができるようになりました。

本日は以上です。

〇コード一覧

#アドオンの定義
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.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():
    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*[\'\"])(.*?)([\'\"])'
    input_matches = re.findall(input_pattern, content)
    output_matches = re.findall(output_pattern,content)
    
    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)
    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)

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()
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()