Stop-Motion Videos Using FFmpeg

Winter is here and my morning project was to swap a set of bindings onto a new snowboard. That’s pretty mundane but I decided I would make a stop motion video of the process.

I set the scene up with my iPhone on a tripod and began snapping a picture at every step. I ended with a 52 photo series. The remaining work was to stitch the photos together in a video.

I thought I could do this right in iMovie on iOS, but I was very wrong. It added a Ken Burns effect to every photo, and a dissolve transition between them. After I painstakingly removed the 104 effects and transitions, I could not find a way to orient the video in portrait to match the photos. iMovie is a little too “on rails” for this.

I went wading through the app store for an app that that could do what I wanted, but it was a sea shoddy freemium apps, and only a few would import from the camera roll. Most required in-app-purchases to enable basic functionality, or to remove their watermark. I tried 7 different apps before remembering FFmpeg, which I had once used for a time-lapse of an ant farm.

For those of you who have not used FFmpeg, it is a total Swiss-army knife for video encoding. It’s quite a popular piece of open source software, and has probably been used by every major media or video-anything company at some point. Most often it is used as a command line tool, which can be a bit intimidating. I’ll show you how to combine frames into video with just a few carefully chosen lines of bash.

First, I Airdropped the photos from my iPhone to my Mac and stuck them in a directory.

I shot the photos with the phone on a DJI Osmo 3 Gimbal and the app produced some pretty gruesome filenames:


Fortunately they were increasing, so listing them in order was automatic. We could also list them in date order with ls -rA1 and then rename the files with a sequence. I didn’t need to do this, but in case you do:

for i in $(ls -rA1); do
  new=$(printf "%04d.jpg" "$a") #04 pad to length of 4
  mv -i -- "$i" "$new"
  let a=a+1

…will get you 0001.jpg, 0002.jpg, …

Next I ran FFmpeg on the files in the directory. I experimented for a good half hour to get this right but I will try to break down the flags:

ffmpeg -f image2 -r 8 -pattern_type glob -i "*.jpg" -aspect 3024:4032 -vf "transpose=1" -r 30 -vcodec libx264 -profile:v main -level 3.1 -preset medium -crf 23 -x264-params ref=4 -acodec copy -movflags +faststart ""

-r 8 The frame rate of input. I found after some experimentation that 8FPS looked good. It depends how many frames you shoot and how much motion occurs between frames. It also affects the length of the output video.

If your files list in order already: -pattern_type glob -i "*.jpg"


If you renamed your files with a sequence number: -i "%04d.jpg" The input pattern. This represents a 4-digit zero-padded number with .jpg at the end.

From manpages (man ffmpeg):

The syntax “foo-%03d.jpeg” specifies to use a decimal number composed of three digits padded with zeroes to express the sequence number. It is the same syntax supported by the C printf function, but only formats accepting a normal integer are suitable.

When importing an image sequence, -i also supports expanding shell-like wildcard patterns (globbing) internally, by selecting the image2-specific “-pattern_type glob” option.

-aspect 3024:4032 Without this, I was getting a slightly squashed video. This was my photo resolution (in portrait). It happens to be 3:4 but who’s counting anyway.

-vf "transpose=1" This is required to rotate the video 90°. I suppose ffmpeg is not reading EXIF data on the image to get the rotation right on it’s own. No sweat.

-r 8 This is the frame rate of the output video. It is a video after all. I set it to match the input frame rate. Any more than that would have no effect.

-vcodec libx264 -profile:v main -level 3.1 -preset medium -crf 23 -x264-params ref=4 -acodec copy -movflags +faststart This is all specific to the video codec used. This configuration works well for most devices.

And finally, my output file is called