Skip to content

Commit 9f39c7e

Browse files
sbryngelsonclaude
andcommitted
Use imageio-ffmpeg for MP4 rendering instead of system ffmpeg
Adds imageio and imageio-ffmpeg as dependencies, which bundles a self-contained ffmpeg binary. Replaces subprocess ffmpeg call and Pillow GIF fallback with imageio's get_writer API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1968272 commit 9f39c7e

2 files changed

Lines changed: 15 additions & 39 deletions

File tree

toolchain/mfc/viz/renderer.py

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"""
88

99
import os
10-
import subprocess
1110

1211
import numpy as np
1312

@@ -198,49 +197,22 @@ def render_mp4(varname, steps, output, fps=10, # pylint: disable=too-many-argum
198197
elif assembled.ndim == 3:
199198
render_3d_slice(assembled, varname, step, frame_path, **opts)
200199

201-
# Combine frames into MP4 using ffmpeg, or fall back to GIF via Pillow
202-
frame_pattern = os.path.join(viz_dir, '%06d.png')
203-
ffmpeg_cmd = [
204-
'ffmpeg', '-y',
205-
'-framerate', str(fps),
206-
'-i', frame_pattern,
207-
'-c:v', 'libx264',
208-
'-pix_fmt', 'yuv420p',
209-
'-vf', 'pad=ceil(iw/2)*2:ceil(ih/2)*2',
210-
output,
211-
]
200+
# Combine frames into MP4 using imageio + imageio-ffmpeg (bundled ffmpeg)
201+
frame_files = sorted(f for f in os.listdir(viz_dir) if f.endswith('.png'))
212202

213203
success = False
214204
try:
215-
subprocess.run(ffmpeg_cmd, check=True, capture_output=True)
205+
import imageio # pylint: disable=import-outside-toplevel
206+
writer = imageio.get_writer(output, fps=fps, codec='libx264',
207+
pixelformat='yuv420p', macro_block_size=2)
208+
for fname in frame_files:
209+
writer.append_data(imageio.imread(os.path.join(viz_dir, fname)))
210+
writer.close()
216211
success = True
217-
except FileNotFoundError:
212+
except ImportError:
218213
pass
219-
except subprocess.CalledProcessError as e:
220-
print(f"ffmpeg failed: {e.stderr.decode()}")
221-
222-
if not success:
223-
# Fall back to GIF via Pillow
224-
gif_output = output.rsplit('.', 1)[0] + '.gif'
225-
try:
226-
from PIL import Image # pylint: disable=import-outside-toplevel
227-
frames = []
228-
frame_files = sorted(f for f in os.listdir(viz_dir) if f.endswith('.png'))
229-
for fname in frame_files:
230-
img = Image.open(os.path.join(viz_dir, fname))
231-
frames.append(img.copy())
232-
img.close()
233-
if frames:
234-
duration = max(int(1000 / fps), 1)
235-
frames[0].save(gif_output, save_all=True, append_images=frames[1:],
236-
duration=duration, loop=0)
237-
output = gif_output
238-
success = True
239-
print(f"ffmpeg not found; saved GIF to {gif_output}")
240-
except ImportError:
241-
print(f"Neither ffmpeg nor Pillow available. Frames saved to {viz_dir}/")
242-
print(f"To create video: ffmpeg -framerate {fps} "
243-
f"-i {frame_pattern} -c:v libx264 -pix_fmt yuv420p {output}")
214+
except Exception as exc: # pylint: disable=broad-except
215+
print(f"imageio MP4 write failed: {exc}")
244216

245217
# Clean up frames
246218
if success:

toolchain/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ dependencies = [
3737
"seaborn",
3838
"matplotlib",
3939

40+
# Visualization (video rendering)
41+
"imageio",
42+
"imageio-ffmpeg",
43+
4044
# Chemistry
4145
"cantera>=3.1.0",
4246
#"pyrometheus == 1.0.5",

0 commit comments

Comments
 (0)