はじめに
- matplotlibでgifアニメーションを作成できるが、長すぎるgif(下記のコードの
end_frame
が大きすぎる場合)を作ろうとするとプロセスがkillされるので困ったapp.pyimport 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を作成するようにした
- アニメーションを分割してmp4として保存
- ffmpegでmp4を結合
- ffmpegでmp4をgifに変換
-
変更後のコードは下記の通り
app.pyimport 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