git clone https://github.com/tanersener/ffmpeg-video-slideshow-scripts.git
为了方便,这里直接使用了conda环境(py10),使用如下代码下载ffmpeg:
conda install -c conda-forge ffmpeg
至此,环境搭建已完成。可以查看ffmpeg的版本等信息。
WIDTH=576
HEIGHT=1024
FPS=30
TRANSITION_DURATION=1
IMAGE_DURATION=2
# FILES=`find ../media/*.jpg | sort -r` # USE ALL IMAGES UNDER THE media FOLDER SORTED
# FILES=('../media/1.jpg' '../media/2.jpg') # USE ONLY THESE IMAGE FILES
FILES=`find ../media/*.jpg` # USE ALL IMAGES UNDER THE media FOLDER
TRANSITION_FRAME_COUNT=$(( TRANSITION_DURATION*FPS ))
IMAGE_FRAME_COUNT=$(( IMAGE_DURATION*FPS ))
TOTAL_DURATION=$(( (IMAGE_DURATION+TRANSITION_DURATION)*IMAGE_COUNT - TRANSITION_DURATION ))
TOTAL_FRAME_COUNT=$(( TOTAL_DURATION*FPS ))
for IMAGE in ${FILES[@]}; do
FULL_SCRIPT+="-loop 1 -i '${IMAGE}' "
done
......
[${c}:v] # 对某个进行输入流进行操作
setpts=PTS-STARTPTS # 时间戳从0开始
scale=w='if(gte(iw/ih,${WIDTH}/${HEIGHT}),min(iw,${WIDTH}),-1)':h='if(gte(iw/ih,${WIDTH}/${HEIGHT}),-1,min(ih,${HEIGHT}))' # 进行缩放,宽高比大于或等于目标宽高比 ${WIDTH}/${HEIGHT},则将宽度调整为 ${WIDTH},高度根据宽高比进行自适应;否则,将高度调整为 ${HEIGHT},宽度根据宽高比进行自适应。
scale=trunc(iw/2)*2:trunc(ih/2)*2 # 将图像的宽度和高度调整为偶数值
pad=width=${WIDTH}:height=${HEIGHT}:x=(${WIDTH}-iw)/2:y=(${HEIGHT}-ih)/2:color=#00000000 # 填充达到指定的宽高(${WIDTH}和${HEIGHT}),填充颜色为透明黑色(#00000000)
setsar=sar=1/1 # 设置样纵横比为1:1,确保输出图像的纵横比与原始图像一致
crop=${WIDTH}:${HEIGHT} # 一个视频处理操作,用于裁剪视频帧的尺寸。
concat=n=3:v=1:a=0 # 连接三个视频片段,只包含视频流而不包含音频流
scale=${WIDTH}*5:-1 # 将连接后的视频流缩放为 ${WIDTH}*5 的宽度,高度根据比例自动调整
zoompan=z='min(pzoom+0.001*${ZOOM_SPEED},2)':d=1:${POSITION_FORMULA}:fps=${FPS}:s=${WIDTH}x${HEIGHT} # min(pzoom+0.001*${ZOOM_SPEED},2) 控制了缩放的速度;${ZOOM_SPEED} 是之前定义的缩放速度变量
[stream$((c+1))] # 得到经过缩放/填充的图像流
for (( c=1; c<${IMAGE_COUNT}; c++ ))
do
FULL_SCRIPT+="[stream${c}overlaid][stream$((c+1))blended]"
done
将图像流通过map直接打印出jpg/png文件。
模板格式如下:
ffmpeg -i 'the path of images' -filter_complex "the operations of ffmpeg" -map "streamout" output1.jpg
例如:
ffmpeg -i '../images/aaa.jpg' -filter_complex "scale=w='if(gte(iw/ih,576/1024),-1,576)':h='if(gte(iw/ih,576/1024),1024,-1)',crop=576:1024,setsar=sar=1/1,fps=30,format=rgba,split=2[stream1out1][stream1out2]" -map "[stream1out1]" output1.jpg -map "[stream1out2]" output2.jpg
# 5张图片为例,总时长 5*2+5-1=14s
WIDTH=1280
HEIGHT=720
FPS=30
TRANSITION_DURATION=1
IMAGE_DURATION=2
# FILE OPTIONS
# FILES=`find ../media/*.jpg | sort -r` # USE ALL IMAGES UNDER THE media FOLDER SORTED
# FILES=('../media/1.jpg' '../media/2.jpg') # USE ONLY THESE IMAGE FILES
FILES=`find ../media/*.jpg` # USE ALL IMAGES UNDER THE media FOLDER
let IMAGE_COUNT=0
for IMAGE in ${FILES[@]}; do (( IMAGE_COUNT+=1 )); done
if [[ ${IMAGE_COUNT} -lt 2 ]]; then
echo "Error: media folder should contain at least two images"
exit 1;
fi
# 输出 图像数量/过渡时间/总时间
TRANSITION_FRAME_COUNT=$(( TRANSITION_DURATION*FPS ))
IMAGE_FRAME_COUNT=$(( IMAGE_DURATION*FPS ))
TOTAL_DURATION=$(( (IMAGE_DURATION+TRANSITION_DURATION)*IMAGE_COUNT - TRANSITION_DURATION ))
TOTAL_FRAME_COUNT=$(( TOTAL_DURATION*FPS ))
echo -e "\nVideo Slideshow Info\n------------------------\nImage count: ${IMAGE_COUNT}\nDimension: ${WIDTH}x${HEIGHT}\nFPS: ${FPS}\nImage duration: ${IMAGE_DURATION} s\n\
Transition duration: ${TRANSITION_DURATION} s\nTotal duration: ${TOTAL_DURATION} s\n"
# SECONDS 是一个内置的 Bash 变量
START_TIME=$SECONDS
所有的命令都是追加到 FULL_SCRIPT 中。
# 1. START COMMAND
FULL_SCRIPT="conda run -n py10 ffmpeg -y " # 需要修改为自己的环境
# 2. ADD INPUTS
# -loop 1 表示将图像文件循环播放
# -i '${IMAGE}' 参数指定输入的图像文件路径
for IMAGE in ${FILES[@]}; do
FULL_SCRIPT+="-loop 1 -i '${IMAGE}' "
done
-filter_complex 参数来处理多个输入流并生成一个或多个输出流的过程。
在这个脚本中,-filter_complex 参数后面的内容将用于定义一系列的滤镜操作,包括图像的缩放、裁剪、设置帧率等
# 3. START FILTER COMPLEX
FULL_SCRIPT+="-filter_complex \""
在循环内部,${c}:v 表示当前图像的视频流索引。然后应用一系列滤镜操作来对该输入流(每个图像都为一个数入流)进行处理,包括图像的缩放、设置像素宽高比、转换为 RGBA 格式、应用模糊效果和设置帧率。
具体的滤镜操作如下:
# 4. PREPARE BLURRED INPUTS
for (( c=0; c<${IMAGE_COUNT}; c++ ))
do
FULL_SCRIPT+="[${c}:v]scale=${WIDTH}x${HEIGHT},setsar=sar=1/1,format=rgba,boxblur=100,setsar=sar=1/1,fps=${FPS}[stream$((c+1))blurred];"
done
循环遍历了每个图像输入流,并为每个输入流执行以下操作:
# 5. PREPARE INPUTS
for (( c=0; c<${IMAGE_COUNT}; c++ ))
do
FULL_SCRIPT+="[${c}:v]scale=w='if(gte(iw/ih,${WIDTH}/${HEIGHT}),min(iw,${WIDTH}),-1)':h='if(gte(iw/ih,${WIDTH}/${HEIGHT}),-1,min(ih,${HEIGHT}))',scale=trunc(iw/2)*2:trunc(ih/2)*2,setsar=sar=1/1,format=rgba[stream$((c+1))raw];"
done
在这一部分,对预处理的模糊图像和缩放后的图像进行叠加操作。
循环遍历了每个图像的模糊图像和缩放后的图像,并使用 overlay 过滤器将它们叠加在一起。叠加操作的参数包括:
# 6. OVERLAY BLURRED AND SCALED INPUTS
for (( c=1; c<=${IMAGE_COUNT}; c++ ))
do
FULL_SCRIPT+="[stream${c}blurred][stream${c}raw]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2:format=rgb,setpts=PTS-STARTPTS,split=2[stream${c}out1][stream${c}out2];"
done
在这一部分,对叠加后的图像进行填充操作。
循环遍历每个图像的叠加后的输出流,并使用 pad 过滤器对图像进行填充。填充操作的参数包括:
# 7. APPLY PADDING
for (( c=1; c<=${IMAGE_COUNT}; c++ ))
do
FULL_SCRIPT+="[stream${c}out1]pad=width=${WIDTH}:height=${HEIGHT}:x=(${WIDTH}-iw)/2:y=(${HEIGHT}-ih)/2,trim=duration=${IMAGE_DURATION},select=lte(n\,${IMAGE_FRAME_COUNT})[stream${c}overlaid];"
if [[ ${c} -eq 1 ]]; then
if [[ ${IMAGE_COUNT} -gt 1 ]]; then
FULL_SCRIPT+="[stream${c}out2]pad=width=${WIDTH}:height=${HEIGHT}:x=(${WIDTH}-iw)/2:y=(${HEIGHT}-ih)/2,trim=duration=${TRANSITION_DURATION},select=lte(n\,${TRANSITION_FRAME_COUNT})[stream${c}ending];"
fi
elif [[ ${c} -lt ${IMAGE_COUNT} ]]; then
FULL_SCRIPT+="[stream${c}out2]pad=width=${WIDTH}:height=${HEIGHT}:x=(${WIDTH}-iw)/2:y=(${HEIGHT}-ih)/2,trim=duration=${TRANSITION_DURATION},select=lte(n\,${TRANSITION_FRAME_COUNT}),split=2[stream${c}starting][stream${c}ending];"
elif [[ ${c} -eq ${IMAGE_COUNT} ]]; then
FULL_SCRIPT+="[stream${c}out2]pad=width=${WIDTH}:height=${HEIGHT}:x=(${WIDTH}-iw)/2:y=(${HEIGHT}-ih)/2,trim=duration=${TRANSITION_DURATION},select=lte(n\,${TRANSITION_FRAME_COUNT})[stream${c}starting];"
fi
done
在这一部分,创建过渡帧(transition frames)。
循环遍历每对相邻的图像,创建过渡效果的帧。对于每对相邻的图像,包括当前图像和下一个图像:
# 8. CREATE TRANSITION FRAMES
for (( c=1; c<${IMAGE_COUNT}; c++ ))
do
FULL_SCRIPT+="[stream$((c+1))starting][stream${c}ending]blend=all_expr='A*(if(gte(T,${TRANSITION_DURATION}),${TRANSITION_DURATION},T/${TRANSITION_DURATION}))+B*(1-(if(gte(T,${TRANSITION_DURATION}),${TRANSITION_DURATION},T/${TRANSITION_DURATION})))',select=lte(n\,${TRANSITION_FRAME_COUNT})[stream$((c+1))blended];"
done
在这一部分,开始进行视频片段的拼接。
循环遍历每对相邻的图像,将前一个图像的叠加输出流(stream c o v e r l a i d )和当前图像的过渡帧输出流( s t r e a m {c}overlaid)和当前图像的过渡帧输出流(stream coverlaid)和当前图像的过渡帧输出流(stream((c+1))blended)连接起来,用于拼接视频片段。
通过这个循环,为每对相邻的图像创建了连接它们的输入流,用于拼接它们之间的视频片段。
# 9. BEGIN CONCAT
for (( c=1; c<${IMAGE_COUNT}; c++ ))
do
FULL_SCRIPT+="[stream${c}overlaid][stream$((c+1))blended]"
done
在这一部分,完成了视频片段的拼接。
将最后一个图像的叠加输出流(stream${IMAGE_COUNT}overlaid)与之前所有连接过渡帧的输出流连接起来,用于拼接所有的视频片段。
通过这个步骤,生成了最终的视频流(video),其中包含了所有图像的叠加和过渡效果。
# 10. END CONCAT
FULL_SCRIPT+="[stream${IMAGE_COUNT}overlaid]concat=n=$((2*IMAGE_COUNT-1)):v=1:a=0,format=yuv420p[video]\""
在这最后一部分,将视频流(video)映射到输出文件,并指定了输出文件的格式和编码参数。
将输出文件的路径设置为…/advanced_blurred_background.mp4,并使用libx264编码器进行视频编码。
其他参数如下:
# 11. END
# FULL_SCRIPT+=" -map [video] -vsync 2 -async 1 -rc-lookahead 0 -g 0 -profile:v main -level 42 -c:v libx264 -r ${FPS} ../advanced_blurred_background.mp4"
FULL_SCRIPT+=" -map [video] -vsync 2 -async 1 -rc-lookahead 0 -g 0 -profile:v main -level 42 -c:v libx264 ../advanced_blurred_background.mp4"
#FULL_SCRIPT+=" -map [video] -async 1 -rc-lookahead 0 -g 0 -profile:v main -level 42 -c:v libx264 -r ${FPS} ../advanced_blurred_background.mp4"
eval ${FULL_SCRIPT}
ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo -e '\nSlideshow created in '$ELAPSED_TIME' seconds\n'
unset $IFS