0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

matplotlibでのgifアニメーション作成が容量大きすぎて失敗したので対処

Last updated at Posted at 2025-01-17

はじめに

  • matplotlibでgifアニメーションを作成できるが、長すぎるgif(下記のコードのend_frameが大きすぎる場合)を作ろうとするとプロセスがkillされるので困った
    app.py
    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.animation import FuncAnimation
    
    def create_gif(start_frame, end_frame, part_number, base_name, fps, gif_format, plot_interval):
        fig, ax = plt.subplots(figsize=(8, 8))
        ax.set_xlim(-200, 200)
        ax.set_ylim(-200, 200)
        ax.set_aspect('equal')
        ax.axis('off')
    
        line, = ax.plot([], [], 'r--', linewidth=2)
    
        a = 1  # Control the distance between turns
    
        def animate(frame):
            t = np.arange(0, start_frame + frame + 1) * plot_interval
            x = a * t * np.cos(t)
            y = a * t * np.sin(t)
            
            line.set_data(x, y)
            
            ax.set_xlim(min(x) - 10, max(x) + 10)
            ax.set_ylim(min(y) - 10, max(y) + 10)
    
            return line,
    
        frames = end_frame - start_frame
        anim = FuncAnimation(fig, animate, frames=frames, interval=20, blit=True)
        gif_name = gif_format.format(base_name=base_name, part_number=part_number)
        anim.save(gif_name, writer='pillow', fps=fps)
        plt.close(fig)
    
    start_frame = 0
    end_frame = 2000
    base_name = "archimedes_spiral"
    fps = 30
    gif_format = "{base_name}_part_{part_number:03d}.gif"
    plot_interval = 0.1
    create_gif(start_frame, end_frame, 0, base_name, fps, gif_format, plot_interval)
    
    
    $ python app.py 
    Killed
    
  • manimの利用も検討したが、仕様がまだ変わりそうなので今回は見送り

解決策

  • 下記の流れでgifを作成するようにした

    1. アニメーションを分割してmp4として保存
    2. ffmpegでmp4を結合
    3. ffmpegでmp4をgifに変換
  • 変更後のコードは下記の通り

    app.py
    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.animation import FuncAnimation, FFMpegWriter
    import subprocess
    import os
    
    def create_mp4(start_frame, end_frame, part_number, base_name, fps, plot_interval):
        fig, ax = plt.subplots(figsize=(8, 8))
        ax.set_xlim(-200, 200)
        ax.set_ylim(-200, 200)
        ax.set_aspect('equal')
        ax.axis('off')
    
        line, = ax.plot([], [], 'r--', linewidth=2)
    
        a = 1  # Control the distance between turns
    
        def animate(frame):
            t = np.arange(0, start_frame + frame + 1) * plot_interval
            x = a * t * np.cos(t)
            y = a * t * np.sin(t)
    
            line.set_data(x, y)
    
            ax.set_xlim(min(x) - 10, max(x) + 10)
            ax.set_ylim(min(y) - 10, max(y) + 10)
    
            return line,
    
        frames = end_frame - start_frame
        anim = FuncAnimation(fig, animate, frames=frames, interval=20, blit=True)
    
        # Save as MP4 using FFMpegWriter
        mp4_name = f"{base_name}_part_{part_number}.mp4"
        writer = FFMpegWriter(fps=fps)
        anim.save(mp4_name, writer=writer)
        
        plt.close(fig)
    
    def create_all_partial(total_frames, frames_per_part, base_name, fps, gif_format, plot_interval):
        for i in range(0, total_frames, frames_per_part):
            start_frame = i
            end_frame = min(i + frames_per_part, total_frames)
            create_mp4(start_frame=start_frame, 
                       end_frame=end_frame, 
                       part_number=i // frames_per_part + 1, 
                       base_name=base_name, 
                       fps=fps, 
                       plot_interval=plot_interval)
    
    def combine_mp4s_and_convert_to_gif(input_prefix, output_filename, max_size_kb=20000, target_width=None, target_height=None, frame_rate=10):
        # Get list of files matching the prefix
        matching_files = [f for f in os.listdir() if f.startswith(input_prefix) and f.endswith('.mp4')]
        
        if not matching_files:
            print(f"No MP4 files found with prefix '{input_prefix}'")
            return
    
        # Sort the files to ensure correct order
        matching_files.sort()
    
        # Create a temporary file with the list of input MP4 files
        with open('input_files.txt', 'w') as f:
            for mp4 in matching_files:
                f.write(f"file '{mp4}'\n")
    
        # Use FFmpeg to concatenate the MP4 files
        combined_mp4 = 'combined.mp4'
        ffmpeg_command = [
            'ffmpeg',
            '-f', 'concat',
            '-safe', '0',
            '-i', 'input_files.txt',
            '-c', 'copy',
            combined_mp4
        ]
    
        try:
            subprocess.run(ffmpeg_command, check=True)
            print(f"Combined MP4 saved as {combined_mp4}")
    
            # Generate a palette for better quality and smaller size
            palette_file = 'palette.png'
            palette_command = [
                'ffmpeg', '-i', combined_mp4,
                '-vf', f'fps={frame_rate},scale={target_width}:{target_height}:flags=lanczos,palettegen',
                palette_file
            ]
            
            subprocess.run(palette_command, check=True)
    
            # Prepare GIF conversion command using the generated palette
            gif_command = [
                'ffmpeg', '-i', combined_mp4,
                '-i', palette_file,
                '-lavfi', f'fps={frame_rate},scale={target_width}:{target_height}:flags=lanczos[x];[x][1:v]paletteuse',
                '-y', output_filename
            ]
    
            subprocess.run(gif_command, check=True)
    
            # Check file size and adjust if necessary (e.g., by reducing frame rate or dimensions)
            while os.path.getsize(output_filename) > max_size_kb * 1024:
                print(f"GIF size exceeds {max_size_kb} KB. Adjusting...")
                frame_rate = max(1, frame_rate - 1)  # Reduce frame rate by 1 fps
                
                gif_command = [
                    'ffmpeg', '-i', combined_mp4,
                    '-i', palette_file,
                    '-lavfi', f'fps={frame_rate},scale={target_width}:{target_height}:flags=lanczos[x];[x][1:v]paletteuse',
                    '-y', output_filename
                ]
                
                subprocess.run(gif_command, check=True)
    
            print(f"Combined GIF saved as {output_filename}")
            
        except subprocess.CalledProcessError as e:
            print(f"Error combining MP4s or converting to GIF: {e}")
    
        # Clean up temporary files
        os.remove('input_files.txt')
        os.remove(combined_mp4)
        os.remove(palette_file)
    
    
    def clean_up_mp4(base_name):
        for file in os.listdir():
            if file.startswith(base_name) and file.endswith('.mp4'):
                os.remove(file)
    
    start_frame = 0
    end_frame = 2000
    base_name = "archimedes_spiral"
    
    fps = 30
    gif_format = "{base_name}_part_{part_number:03d}.gif"
    plot_interval = 0.1
    
    total_frames = end_frame - start_frame
    frames_per_part = 500
    create_all_partial(total_frames, frames_per_part, base_name, fps, gif_format, plot_interval)
    combine_mp4s_and_convert_to_gif(
        base_name + "_", 
        output_name + ".gif", 
        max_size_kb=10000, 
        target_width=320, 
        target_height=320, frame_rate=10)
    clean_up_mp4(base_name)
    
  • 実行のためのDockerfileはこちら

    Dockerfile
    # Use an official Python runtime as a parent image
    FROM python:3.9-slim
    
    # Set the working directory in the container
    WORKDIR /app
    
    # Copy the current directory contents into the container at /app
    COPY . /app
    
    # Install FFmpeg and any needed packages specified in requirements.txt
    RUN apt-get update && apt-get install -y ffmpeg && \
        pip install --no-cache-dir matplotlib numpy pillow
    

出力されるgif

archimedes_spiral_final.gif
(表示に時間がかかるかも)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?