Files
2026-08-03 18:42:47 +08:00

991 lines
31 KiB
Python

#A* -------------------------------------------------------------------
#B* This file contains source code for the PyMOL computer program
#C* Copyright (c) Schrodinger, LLC.
#D* -------------------------------------------------------------------
#E* It is unlawful to modify or remove this copyright notice.
#F* -------------------------------------------------------------------
#G* Please see the accompanying LICENSE file for further information.
#H* -------------------------------------------------------------------
#I* Additional authors of this source file include:
#-* Peter Haebel, Byron DeLaBarre
#-*
#-*
#Z* -------------------------------------------------------------------
import sys
cmd = sys.modules["pymol.cmd"]
import math
import os
import glob
import shutil
import threading
import time
from . import colorprinting
def get_movie_fps(_self):
r = _self.get_setting_float('movie_fps')
if r <= 0:
return 30
return r
def sweep(pause=0,cycles=1,_self=cmd):
pause = int(pause)
cycles = int(cycles)
n_state = _self.count_states("all")
if pause>0:
pass_string = "1 x%d 1 -%d %d x%d %d -1"%(pause, n_state, n_state, pause, n_state)
else:
pass_string = "1 -%d %d -1"%(n_state, n_state)
movie_list = [ pass_string ] * cycles
movie_string = " ".join(movie_list)
_self.mset(movie_string)
def pause(pause=15,cycles=1,_self=cmd):
pause = int(pause)
cycles = int(cycles)
n_state = _self.count_states("all")
if pause>0:
pass_string = "1 x%d 1 -%d %d x%d"%(pause, n_state, n_state, pause)
else:
pass_string = "1 -%d %d -1"%(n_state, n_state)
movie_list = [ pass_string ] * cycles
movie_string = " ".join(movie_list)
_self.mset(movie_string)
def load(pattern, nam = "mov", _self=cmd, **kw):
fils = glob.glob(pattern)
if not fils:
print("Error: no matching files")
return
for a in sorted(fils):
_self.load(a, nam, **kw)
def rock(first=1,last=-1,angle=30,phase=0,loop=1,axis='y',_self=cmd):
first=int(first)
last=int(last)
if last<0:
last = _self.count_frames()
angle=float(angle)
phase=float(phase)
loop=int(loop)
nstep = (last-first)+1
if nstep<0:
nstep = 1
if loop:
subdiv = nstep
else:
subdiv = nstep+1
ang_cur = math.pi*phase/180
ang_inc = 2*math.pi/subdiv
ang_cur = ang_cur - ang_inc
a = 0
while a<nstep:
last = angle*math.sin(ang_cur)/2
ang_cur = ang_cur + ang_inc
disp = angle*math.sin(ang_cur)/2
diff = disp-last
# com = "mdo %d:turn %s,%8.3f" % (first+a,axis,diff)
# _self.do(com)
_self.mdo("%d"%(first+a),"turn %s,%8.3f"% (axis,diff))
a = a + 1
def roll(first=1,last=-1,loop=1,axis='y',_self=cmd):
first=int(first)
last=int(last)
loop=int(loop)
axis=str(axis)
if last<0:
last = _self.count_frames()
n = last - first
if loop:
step = 2*math.pi/(n+1)
else:
step = 2*math.pi/n
a = 0
invert = 1
if axis[0:1]=='-':
axis=axis[1:]
invert = -1
deg = invert * (180*step/math.pi)
while a<=n:
# com = "mdo %d:turn %s,%8.3f" % (first+a,axis,deg)
# _self.do(com)
_self.mdo("%d" % (first+a), "turn %s,%8.3f" % (axis,deg))
a = a + 1
def tdroll(first,rangex,rangey,rangez,skip=1,_self=cmd):
'''
AUTHOR
Byron DeLaBarre
USAGE
movie.tdroll(rangx,rangey,rangez,skip=1,mset=0)
rangex/y/z = rotation range on respective axis
enter 0 for no rotation.
skip is angle increment in each frame
Use skip to reduce final movie size or to speed up rotation.
EXAMPLE
movie.tdroll 360,360,360,5
'''
first = int(first)
rangex = float(rangex)
rangey = float(rangey)
rangez = float(rangez)
skip = int(skip)
axis=['x','y','z']
rangel=[rangex,rangey,rangez]
axpos=0
frpos=1
for ax in axis:
range = int(rangel[axpos])
if range:
leftover = divmod(range,skip)
print(leftover[1])
if leftover[1]:
range = range + int(leftover[1])
a = 0
while a<range:
_self.mdo("%d" % (first+frpos-1), "turn %s,%8.3f" % (ax,skip))
a = a + skip
frpos = frpos + 1
axpos = axpos + 1
else:
axpos = axpos + 1
print((" tdroll: defined rotations for", frpos - 1,
"frames, starting at frame %d"%first))
def zoom(first,last,step=1,loop=1,axis='z',_self=cmd):
# Author: Peter Haebel
first=int(first)
last=int(last)
step=int(step)
loop=int(loop)
n = last - first
a = 0
while a<=n:
if (loop and a>n/2):
s = -step
else:
s = step
# com = "mdo %d:move %s,%8.3f" % (first+a,axis,s)
# _self.do(com)
_self.mdo("%d" % (first+a),"move %s,%8.3f" % (axis,s))
a = a + 1
def nutate(first,last,angle=30,phase=0,loop=1,shift=math.pi/2.0,_self=cmd):
first=int(first)
last=int(last)
angle=float(angle)
phase=float(phase)
loop=int(loop)
nstep = (last-first)+1
if nstep<0:
nstep = 1
if loop:
subdiv = nstep
else:
subdiv = nstep+1
ang_cur = math.pi*phase/180
ang_inc = 2*math.pi/subdiv
ang_cur = ang_cur - ang_inc
a = 0
while a<nstep:
lastx = angle*math.sin(ang_cur)/2
lasty = angle*math.sin(ang_cur+shift)/2
ang_cur = ang_cur + ang_inc
nextx = angle*math.sin(ang_cur)/2
nexty = angle*math.sin(ang_cur+shift)/2
# com = "mdo %d:turn %s,%8.3f" % (first+a,axis,diff)
# _self.do(com)
_self.mdo("%d"%(first+a),"turn x,%8.3f;turn y,%8.3f;turn y,%8.3f;turn x,%8.3f"%
(-lastx,-lasty,nexty,nextx))
a = a + 1
def screw(first,last,step=1,angle=30,phase=0,loop=1,axis='y',_self=cmd):
# Author: Peter Haebel
first=int(first)
last=int(last)
step=int(step)
angle=float(angle)
phase=float(phase)
loop=int(loop)
nstep = (last-first)+1
if nstep<0:
nstep = 1
if loop:
subdiv = nstep
else:
subdiv = nstep+1
ang_cur = math.pi*phase/180
ang_inc = 2*math.pi/subdiv
ang_cur = ang_cur - ang_inc
a = 0
while a<nstep:
if (loop and a>=nstep/2):
s = -step
else:
s = step
last = angle*math.sin(ang_cur)/2
ang_cur = ang_cur + ang_inc
disp = angle*math.sin(ang_cur)/2
diff = disp-last
# com = "mdo %d:turn %s,%8.3f; move z,%8.3f" % (first+a,axis,diff,s)
# _self.do(com)
_self.mdo("%d" % (first+a), "turn %s,%8.3f; move z,%8.3f" % (axis,diff,s))
a = a + 1
def timed_roll(period=12.0,cycles=1,axis='y',_self=cmd):
frames_per_sec = get_movie_fps(_self)
if frames_per_sec<1.0:
frames_per_sec=30.0
frames_per_cycle = int(period*frames_per_sec)
total = frames_per_cycle * cycles
_self.mset("1 x%d"%total)
step = 2*math.pi/(frames_per_cycle)
deg = (180*step/math.pi)
_self.mview('reset')
_self.rewind()
frame = 1
for cycle in range(cycles):
for cnt in range(frames_per_cycle):
_self.turn(axis,deg)
_self.mview('store',frame,freeze=1)
frame = frame + 1
# blank frames to be specified by the user
def add_blank(duration=12.0,start=0,_self=cmd):
'''
DESCRIPTION
Add "blank" time to the movie (without any key frames)
ARGUMENTS
duration = float: time to add to movie in seconds {default: 12}
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
SEE ALSO
mset
'''
cmd = _self
if not start:
start = cmd.get_movie_length()+1
duration = float(duration)
fps = get_movie_fps(_self)
n_frame = int(round(fps * duration))
if n_frame > 0:
cmd.mset("1 x%d"%n_frame,start)
cmd.frame(start)
# new matrix-based camera interpolation routines
def add_roll(duration=12.0,loop=1,axis='y',start=0,_self=cmd):
'''
DESCRIPTION
Append a 360 degree camera rotation to the movie, using key frames.
ARGUMENTS
duration = float: time to add to movie in seconds {default: 12}
loop = 0/1: ???
axis = x, y or z: rotation axis in camera space {default: y}
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
'''
cmd = _self
if not start:
start = cmd.get_movie_length()+1
duration = float(duration)
fps = get_movie_fps(_self)
n_frame = int(round(fps * duration))
if n_frame > 0:
cmd.mset("1 x%d"%n_frame,start,freeze=1)
cmd.mview("store",start,power=1,freeze=1)
cmd.turn(axis,120)
cmd.mview("store",start+n_frame/3,power=1,freeze=1)
cmd.turn(axis,120)
cmd.mview("store",start+(2*n_frame)/3,power=1,freeze=1)
if loop:
if (start == 1):
cmd.mview("interpolate",wrap=1)
cmd.turn(axis,120)
cmd.mview("store",start+n_frame-1,power=1,freeze=1)
cmd.turn(axis,120)
else:
adjustment = 360.0/n_frame
cmd.turn(axis,120 - adjustment)
cmd.mview("store",start+n_frame-1,power=1,freeze=1)
cmd.mview("interpolate")
cmd.turn(axis,adjustment)
else:
cmd.turn(axis,120)
cmd.mview("store",start+n_frame-1,power=1,freeze=1)
cmd.mview("interpolate")
cmd.frame(start)
# PYMOL-2881
if cmd.get_setting_int('movie_auto_interpolate'):
cmd.mview("reinterpolate")
def add_rock(duration=8.0,angle=30.0,loop=1,axis='y',start=0,_self=cmd):
'''
DESCRIPTION
Append a rocking camera motion to the movie, using key frames.
ARGUMENTS
duration = float: time to add to movie in seconds {default: 8}
angle = float: degrees {default: 30}
loop = 0/1: ???
axis = x, y or z: rotation axis in camera space {default: y}
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
'''
cmd = _self
if not start:
start = cmd.get_movie_length()+1
duration = float(duration)
angle=float(angle)
loop=int(loop)
fps = get_movie_fps(_self)
n_frame = int(round(fps * duration))
if n_frame > 0:
cmd.mset("1 x%d"%n_frame,start,freeze=1)
cmd.turn(axis,angle/2.0)
cmd.mview("store",start+n_frame/4,power=-1,freeze=1)
cmd.turn(axis,-angle)
cmd.mview("store",start+(3*n_frame)/4,power=-1,freeze=1)
if loop and (start == 1):
cmd.mview("interpolate",wrap=1)
else:
cmd.mview("interpolate")
cmd.frame(start)
def add_state_sweep(factor=1,pause=2.0,first=-1,last=-1,loop=1,start=0,_self=cmd):
cmd = _self
if not start:
start = cmd.get_movie_length() + 1
loop = int(loop)
fps = get_movie_fps(_self)
n_state = cmd.count_states()
duration = (2 * pause) + (2 * factor * n_state) / fps
n_frame = int(round(fps * duration))
if n_frame > 0:
cmd.mset("1 x%d"%n_frame, start, freeze=1)
cmd.mview("store",start, state=1, freeze=1)
cmd.mview("store",start + (n_frame * pause) / duration, state=1, freeze=1)
cmd.mview("store",start + n_state * factor + (n_frame * pause) / duration - 1, state=n_state, freeze=1)
cmd.mview("store",start + n_state * factor + (2 * n_frame * pause) / duration, state=n_state, freeze=1)
cmd.mview("store",start + n_frame-1, state=1, freeze=1)
if loop and (start == 1):
cmd.mview("interpolate",wrap=1)
else:
cmd.mview("interpolate")
cmd.frame(start)
# PYMOL-2881
if cmd.get_setting_int('movie_auto_interpolate'):
cmd.mview("reinterpolate")
def add_state_loop(factor=1,pause=2.0,first=-1,last=-1,loop=1,start=0,_self=cmd):
cmd = _self
if not start:
start = cmd.get_movie_length() + 1
loop = int(loop)
fps = get_movie_fps(_self)
n_state = cmd.count_states()
duration = (pause) + (factor * n_state) / fps
n_frame = int(round(fps * duration))
if n_frame > 0:
cmd.mset("1 x%d"%n_frame, start, freeze=1)
cmd.mview("store",start, state=1, freeze=1)
cmd.mview("store",start + (n_frame * pause * 0.5) / duration, state=1, freeze=1)
cmd.mview("store",start + n_state * factor + (n_frame * pause * 0.5) / duration, state=n_state, freeze=1)
cmd.mview("store",start + n_frame-1, state=n_state, freeze=1)
if loop and (start == 1):
cmd.mview("interpolate",wrap=1)
else:
cmd.mview("interpolate")
cmd.frame(start)
# PYMOL-2881
if cmd.get_setting_int('movie_auto_interpolate'):
cmd.mview("reinterpolate")
def add_nutate(duration=8.0, angle=30.0, spiral=0, loop=1,
offset=0, phase=0, shift=math.pi/2.0, start=0,
_self=cmd):
'''
DESCRIPTION
Append a nutating camera motion to the movie, using key frames.
ARGUMENTS
duration = float: time to add to movie in seconds {default: 8}
angle = float: degrees {default: 30}
spiral = -1/0/1: If zero, do a circular motion, otherwise do a spiral
motion starting at the center (clockwise if 1, ccw if -1) {default: 0}
loop: (unused)
offset: (unused)
phase = float: phase offset in degrees {default: 0}
shift = float: x to y offset in radians {default: pi/2}
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
'''
cmd = _self
if not start:
start = cmd.get_movie_length()+1
duration = float(duration)
angle = float(angle)
spiral = int(spiral)
loop = int(loop)
fps = get_movie_fps(_self)
n_frame = int(round(fps * duration))
if n_frame > 0:
cmd.mset("1 x%d"%n_frame,start,freeze=1)
for index in range(0,n_frame):
if spiral>0:
sp_angle = angle*float(index+1)/n_frame
elif spiral<0:
sp_angle = angle*(1.0 - float(index+1)/n_frame)
else:
sp_angle = angle
ang_cur = math.pi*phase/180.0 + (2*math.pi*index)/n_frame
x_rot = sp_angle * math.sin(ang_cur)/2
y_rot = sp_angle * math.sin(ang_cur+shift)/2
cmd.turn('x',x_rot)
cmd.turn('y',y_rot)
cmd.mview('store',start+index,freeze=1)
cmd.turn('y',-y_rot)
cmd.turn('x',-x_rot)
# PYMOL-2881
if cmd.get_setting_int('movie_auto_interpolate'):
cmd.mview("reinterpolate")
def _rock(mode,axis,first,last,period,pause,_self=cmd):
cmd = _self
n_frame = last - first + 1
angle = cmd.get_setting_float('sweep_angle')
if (period * 1.5) < pause:
n_cyc = int(round(pause / period))
frame_list = []
for cyc in range(n_cyc):
frame_list.extend( [(first + ((1+4*cyc)*n_frame)/(4*n_cyc)),
(first + ((3+4*cyc)*n_frame)/(4*n_cyc))] )
else:
frame_list = [ first+n_frame/4, first+(3*n_frame)/4 ]
if 1 or mode:
direction = 0
for frame in frame_list:
if not direction:
cmd.turn(axis,angle/2.0)
cmd.mview("store",frame,power=-1,freeze=1)
direction = -1
else:
cmd.turn(axis, direction * angle)
cmd.mview("store",frame,power=-1,freeze=1)
direction = -direction
cmd.turn(axis,direction * angle/2.0)
cmd.mview("store",last,power=-1,freeze=1)
cmd.mview("interpolate",first,last)
def _nutate_sub(start_frame, stop_frame, angle=15.0, spiral=0, loop=1,
offset=0, phase=0, shift=math.pi/2.0, _self=cmd):
cmd = _self
angle = float(angle)
spiral = int(spiral)
loop = int(loop)
fps = get_movie_fps(_self)
duration = (stop_frame - start_frame)/fps
n_frame = int(round(fps * duration))
if n_frame > 0:
for index in range(0,n_frame):
if spiral>0:
sp_angle = angle*float(index+1)/n_frame
elif spiral<0:
sp_angle = angle*(1.0 - float(index+1)/n_frame)
else:
sp_angle = angle
ang_cur = math.pi*phase/180.0 + (2*math.pi*index)/n_frame
x_rot = sp_angle * math.sin(ang_cur)/2
y_rot = sp_angle * math.sin(ang_cur+shift)/2
cmd.turn('x',x_rot)
cmd.turn('y',y_rot)
cmd.mview('store',start_frame+index,freeze=1)
cmd.turn('y',-y_rot)
cmd.turn('x',-x_rot)
def _nutate(mode,first,last,period,pause,_self=cmd):
cmd = _self
n_frame = last - first + 1
axis = 'y'
angle = cmd.get_setting_float('sweep_angle')
if (period * 1.5) < pause:
n_cyc = int(round(pause / period))
frame_list = []
for cyc in range(n_cyc):
frame_list.append( [(first + ((cyc)*n_frame)/(n_cyc)),
(first + ((cyc+1)*n_frame)/(n_cyc))] )
else:
frame_list = [ [first, first+n_frame] ]
direction = 0
spiral = 1
for frame in frame_list:
_nutate_sub(frame[0], frame[1], angle, spiral, _self=_self)
spiral = 0
def add_scenes(names=None, pause=8.0, cut=0.0, loop=1,
rock=-1, period=8.0, animate=-1, start=0,
_self=cmd):
'''
DESCRIPTION
Append a sequence of scenes to the movie, with camera animation
(rock/nutate) at each scene.
ARGUMENTS
names = list: list of scenes names {default: all scenes}
pause = float: display time per scene in seconds {default: 8}
cut = float 0.0-1.0: scene switch moment (0.0: beginning of transition,
1.0: end of transition) {default: 0.0}
loop = 0/1: end of movie interpolates back to first frame {default: 1}
rock = int: (sweep_mode + 1): 2=x-axis-rock, 3=y-axis-rock, 4=nutate
{default: -1, use sweep_mode setting}
period = float: rock/nutate time (e.g. "period=2, pause=6" will rock 3
times per scene) {default: 8}
animate = float: scene transition time in seconds
{default: -1, use scene_animation_duration setting}
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
'''
cmd = _self
if not start:
start = cmd.get_movie_length()+1
animate = float(animate)
pause = float(pause)
period = float(period)
if period>pause:
period = pause
rock = int(rock)
if animate<0:
animate = float(cmd.get("scene_animation_duration"))
if names is None:
names = cmd.get_scene_list()
elif cmd.is_string(names):
names = cmd.safe_alpha_list_eval(names)
n_scene = len(names)
duration = n_scene*(pause+animate)
fps = get_movie_fps(_self)
n_frame = int(round(fps * duration))
if not loop:
act_n_frame = int(round(fps * (duration - animate)))
else:
act_n_frame = n_frame
if rock<0:
sweep_mode = int(cmd.get('sweep_mode'))
elif rock:
sweep_mode = rock - 1
else:
sweep_mode = 0
if n_frame > 0:
cmd.mset("1 x%d"%act_n_frame,start,freeze=1)
cnt = 0
for scene in names:
frame = start+int((cnt*n_frame)/n_scene)
cmd.mview("store",frame,scene=scene,freeze=1)
if rock:
cmd.mview("interpolate",cut=cut,wrap=0)
sweep_first = frame + 1
sweep_last = start + int(pause*fps+(cnt*n_frame)/n_scene) - 1
if sweep_last > act_n_frame:
sweep_last = act_n_frame
n_sweep_frame = sweep_last - sweep_first + 1
if n_sweep_frame > 0:
if sweep_mode==1: # x-axis rock
_rock(sweep_mode, 'x', sweep_first, sweep_last,
period, pause, _self=_self)
elif sweep_mode<3: # y-axis rock
_rock(sweep_mode, 'y', sweep_first, sweep_last,
period, pause, _self=_self)
elif sweep_mode == 3:
_nutate(sweep_mode, sweep_first, sweep_last,
period, pause, _self=_self)
frame = start+int(pause*fps+(cnt*n_frame)/n_scene)
if frame <= act_n_frame:
if sweep_mode!=3:
cmd.mview("store",frame,scene=scene,freeze=1)
cnt = cnt + 1
cmd.mview("interpolate",cut=cut,wrap=loop)
if rock:
cmd.mview("smooth")
cmd.frame(start)
_prefix = "mov"
def _watch(filename,done_event):
tries = 5
size = 0
import time,os
while not os.path.exists(filename):
if done_event.isSet():
break
tries = tries - 1
if tries < 0:
break
time.sleep(1)
if os.path.exists(filename):
tries = 5
while os.path.exists(filename):
stat = os.stat(filename)
if size != stat[6]:
tries = 5
size = stat[6]
if done_event.isSet():
break
print(" produce: %d bytes written..."%size)
else:
tries = tries - 1
if tries < 0:
break
time.sleep(2)
if done_event.isSet():
break
def _encode(filename,first,last,preserve,
encoder,tmp_path,prefix,img_ext,quality,quiet,_self=cmd):
import os
tries = 10
while 1: # loop until all of the files have been created...
done = 1
# check for the required output files
for index in range(first,last+1):
path = os.path.join(tmp_path, "%s%04d%s" % (prefix, index, img_ext))
if not os.path.exists(path):
done = 0
break;
if done:
break
elif _self.get_modal_draw(): # keep looping so long as we're rendering...
tries = 10
else:
tries = tries - 1
if tries < 0:
done = 0
break
time.sleep(0.25)
_self.sync()
ok = 1
result = None
# reduce chance of passing non-ascii file paths to sub processes
# by changing directory
fn_rel = os.path.relpath(filename, tmp_path)
old_cwd = os.getcwd()
fps = get_movie_fps(_self)
if done and ok and (encoder == 'mpeg_encode'):
try:
from pymol import mpeg_encode
except:
ok = 0
print("produce-error: Unable to import module pymol.mpeg_encode.")
if ok:
if not mpeg_encode.validate():
ok = 0
print("produce-error: Unable to validate pymol.mpeg_encode.")
if not ok:
print("produce-error: Unable to create mpeg file.")
else:
mpeg_quality = 1+int(((100-quality)*29)/100) # 1 to 30
input = mpeg_encode.input(fn_rel, '.',
prefix,first,last,mpeg_quality);
FPS_LEGAL_VALUES = [23.976, 24, 25, 29.97, 30, 50, 59.94, 60]
fps_legal = min(FPS_LEGAL_VALUES, key=lambda v: abs(v - fps))
if fps_legal != round(fps, 3):
colorprinting.warning(
" Warning: Adjusting frame rate to {} fps (legal values are: {})"
.format(fps_legal, FPS_LEGAL_VALUES))
input = input.replace('FRAME_RATE 30',
'FRAME_RATE {:.3f}'.format(fps_legal))
if not quiet:
print(" produce: creating '%s' (in background)..."%(filename))
os.chdir(tmp_path)
done_event = None
if not quiet:
done_event = threading.Event()
_self.async_(_watch, fn_rel, done_event, _self=_self)
try:
result = mpeg_encode.run(input)
finally:
os.chdir(old_cwd)
if done_event is not None:
done_event.set()
elif encoder == 'ffmpeg':
import subprocess
os.chdir(tmp_path)
try:
if fn_rel.endswith('.gif'):
palette = os.path.join(tmp_path, 'palette.png')
filters = f"fps={fps}"
process = subprocess.Popen(['ffmpeg', '-v', 'warning', '-thread_queue_size', '512', '-i', prefix + '%04d' + img_ext, '-vf', filters+',palettegen', '-y', palette], stderr=subprocess.PIPE)
stderr = process.communicate()[1]
colorprinting.warning(stderr.strip().decode(errors='replace'))
process = subprocess.Popen(['ffmpeg', '-v', 'warning', '-thread_queue_size', '512','-i', prefix + '%04d' + img_ext, '-i', palette, '-lavfi', filters+' [x]; [x][1:v] paletteuse', '-y', fn_rel], stderr=subprocess.PIPE)
else:
args = ['ffmpeg',
'-f', 'image2',
'-framerate', '{:.3f}'.format(fps),
'-i', prefix + '%04d' + img_ext,
]
if fn_rel.endswith('.webm'):
args_crf = ['-crf', '{:.0f}'.format(65 - (quality / 2))]
args += ['-c:v', 'libvpx-vp9', '-b:v', '0'] + args_crf
else:
args += [
'-crf', '10' if quality > 90 else '15' if quality > 80 else '20',
'-pix_fmt', 'yuv420p', # needed for Mac support
]
process = subprocess.Popen(args + [fn_rel], stderr=subprocess.PIPE)
stderr = process.communicate()[1]
colorprinting.warning(stderr.strip().decode(errors='replace'))
if process.returncode != 0:
colorprinting.error('ffmpeg failed with '
'exit status {}'.format(process.returncode))
finally:
os.chdir(old_cwd)
elif encoder == 'convert':
import subprocess
exe = find_exe(encoder)
try:
subprocess.check_call([exe,
'-delay', '{:.3f}'.format(100. / fps), # framerate
os.path.join(tmp_path, prefix) + '*' + img_ext,
filename])
finally:
pass
if not quiet:
if not os.path.exists(filename):
if result is not None:
print(input, result[0], result[1])
print(" produce: compression failed")
else:
print(" produce: finished.")
_self.unset("keep_alive")
if preserve<1:
shutil.rmtree(tmp_path)
produce_mode_dict = {
'normal' : 0,
'draw' : 1,
'ray' : 2,
}
produce_mode_sc = cmd.Shortcut(produce_mode_dict.keys())
def find_exe(exe):
r'''Return full path to executable or None.
Excludes C:\Windows\System32\convert.exe
Tests .exe extension on Unix (e.g. for legacy "mpeg_encode.exe" name).
'''
from shutil import which
if exe.startswith('convert') and sys.platform == 'win32':
# filter out C:\Windows\System32
path = os.pathsep.join(p
for p in os.getenv('PATH', '').split(os.pathsep)
if r'\windows\system32' not in p.lower())
return which(exe, path=path)
if exe == 'mpeg_encode':
legacy = which(exe + '.exe')
if legacy:
return legacy
return which(exe)
def produce(filename, mode='', first=0, last=0, preserve=0,
encoder='', quality=-1, quiet=1,
width=0, height=0, _self=cmd):
'''
DESCRIPTION
Export a movie to an MPEG, WEBM, or GIF file.
Which video formats and codecs are available depends on the availability
and feature set of your ffmpeg and ImageMagick installations.
ARGUMENTS
filename = str: filename of movie file to produce
mode = draw or ray: {default: check "ray_trace_frames" setting}
first = int: first frame to export {default: 1}
last = int: last frame to export {default: last frame of movie}
preserve = 0 or 1: don't delete temporary files {default: 0}
encoder = ffmpeg|convert|mpeg_encode: Tool used for video encoding
quality = 0-100: encoding quality {default: 90 (movie_quality setting)}
width = int: Width in pixels {default: from viewport}
height = int: Height in pixels {default: from viewport}
EXAMPLE
movie.produce video.mp4, height=1080
movie.produce video.webm, height=720
'''
from pymol import CmdException
def has_exe(exe):
return bool(find_exe(exe))
prefix = _prefix
if mode == '':
mode = -1
elif _self.is_string(mode):
mode = produce_mode_sc.auto_err(mode,"mode")
mode = produce_mode_dict[mode]
else:
mode = int(mode)
first = int(first)
last = int(last)
quiet = int(quiet)
preserve = int(preserve)
quality = int(quality)
if quality<0:
quality = _self.get_setting_int('movie_quality')
if quality>100:
quality = 100
ok = 1
splitext = os.path.splitext(filename)
tmp_path = splitext[0]+".tmp"
if splitext[1]=='':
splitext=(splitext[0],'.mpg')
filename = splitext[0]+splitext[1]
width, height = int(width), int(height)
img_ext = '.png'
# guess encoder
if not encoder:
if splitext[1] in ('.mpeg', '.mpg'):
encoder = 'mpeg_encode'
elif has_exe('ffmpeg'):
encoder = 'ffmpeg'
elif has_exe('convert'):
encoder = 'convert'
else:
raise CmdException('neither "ffmpeg" nor "convert" available for '
'video encoding')
print('using encoder "%s"' % encoder)
# check encoder
if encoder == 'mpeg_encode':
img_ext = '.ppm'
elif encoder not in ('ffmpeg', 'convert'):
raise CmdException('unknown encoder "%s"' % encoder)
elif not has_exe(encoder):
raise CmdException('encoder "%s" not available' % (encoder))
if img_ext == '.png':
_self.set('opaque_background', quiet=quiet)
# MP4 needs dimensions divisible by 2
if splitext[1] in ('.mp4', '.mov', '.webm'):
if width < 1 or height < 1:
w, h = _self.get_viewport()
if width > 0:
height = width * h / w
elif height > 0:
width = height * w / h
else:
width, height = w, h
if width % 2:
width -= 1
if height % 2:
height -= 1
# clean up old files if necessary
if os.path.exists(filename):
os.unlink(filename)
if not os.path.exists(tmp_path):
os.mkdir(tmp_path)
elif preserve==0:
# get rid of existing frames (if they exist)
for fil in glob.glob(os.path.join(tmp_path,prefix+"*")):
os.unlink(fil)
if preserve<0:
preserve = 0
if os.path.isdir(tmp_path):
if first <= 0:
first = 1
if last <= 0:
last = _self.count_frames()
if last <= 1:
last = 1
_self.set("keep_alive")
_self.mpng(os.path.join(tmp_path,prefix + img_ext),first,last,
preserve,mode=mode,modal=-1,quiet=quiet,
width=width, height=height)
# this may run asynchronously
else:
ok = 0
if ok:
args = ((filename, first, last, preserve,
encoder,tmp_path,prefix,img_ext,
quality,quiet,_self))
if _self.get_modal_draw():
t = threading.Thread(target=_encode, args=args)
t.setDaemon(1)
t.start()
else:
_encode(*args)
if ok:
return _self.DEFAULT_SUCCESS
else:
return _self.DEFAULT_ERROR