Video Stabilization

Recently I purchased a kayak. I wanted to take photos with my GoPro mounted to the top. Seemed simple enough. It was, and thus was totally unacceptable.

Let’s make this user story more complicated.

The Story

I wanted to be able to create a time lapse video quickly to send to my friends. From there, I’d send the video to anyone who was with me that day. If they wanted an original high resolution image used to create the video, they could give me the time stamp. From there, I could send the dozen or so images that created that second of video easily.

Put another way: I was in essence creating a quick way to preview the thousands of photos GoPro can take in a short time.

Naïve First Try

Seems simple enough:

  1. Hit record on GoPro
  2. Kayak
  3. Download Images
  4. Let GoPro Studio do its thing

That didn’t work.

That shakiness makes it impossible to watch. Ain’t nobody got no time for that.

Second Pass

I came to realizing no filter GoPro could provide would help that. Manually rotating and lining up all frames would take forever. Time to automate.

First I created script stabilize:

# Copyright 2016 Tim Doerzbacher <tim at>

# Stabilization options
MELT_FILTERS="-filter vidstab shakiness=8 smoothing=20 optzoom=0 maxangle=0.05"
# Encoding options
MELT_OPTIONS="vcodec=libx264 b=16000k acodec=aac ab=128k tune=film preset=slow"

if [ -z "$1" ] ; then
  echo "Usage: $0 <infile> [outfile]"
  exit 1434;

DEST="`echo $1 | sed -r 's/\.(mp4|mpg|mov)/.stabilized.m4v/'`"
MLT_FILE="`echo $1 | sed -r 's/\.(mp4|mpg|mov)/.stabilized.mlt/'`"

if [ ! -z "$2" ] ; then

if [ "$SRC" = "$DEST" ]; then
  echo "Did not recognize that file type."
  exit 1 # Not random

echo "Stabilizing: ${SRC}"
echo "Destination: ${DEST}"

echo "======== Round #1 ========"
$MELT "$SRC" $MELT_FILTERS -consumer "xml:${MLT_FILE}" all=1

echo "======== Round #2 ========"
$MELT "$MLT_FILE" -audio-track "$SRC" -consumer "avformat:${DEST}" $MELT_OPTIONS

So the new order of things was:

  1. Hit record on GoPro
  2. Kayak
  3. Download Images
  4. Let GoPro Studio do its thing
  5. Process resultant video with stabilize

This method left a bit of weirdness at the edges. I tried using the auto-zoom functionality but didn’t like the result. Since GoPro studio already threw out a lot of the original resolution, zooming in further throws away even more information. The end result is far from optimal and looks blurry:

There Must Be a Better Way

There was a better way. I have a large amount of sequentially named files and a snazzy Linux server. I’m losing a lot of resolution by stabilizing the video after its already been scaled down.

Time to make a new script: encode.

$FFMPEG -pattern_type glob -i '*.JPG' -c:v libx264 video.mp4

Now I have an easy way to create one video out of all images taken that day.

$ encode JPG; stabilize video.mp4

At this point, I can take video.stabilized.m4v and import it into GoPro. Now I have full resolution video that I can then crop as necessary. The result works out quite a bit better:

Still, I’m having problems smoothing the video out. At this point, I began I wonder how it would work with better data. The water drops taking up so much of the field of view. It is probably affecting the stabilization.

Better Data

A few days later I had the opportunity to revisit the lake to do some plein air painting.

After that was done, I recorded and processed another time lapse using the scripts above:

Ultimately, I’m pretty happy with the last few attempts at making a kayak trip bearable. Things are still not perfect. I’m not aware of a way to make the GoPro take more than two frames per second. That’s causing the Flux processing in GoPro Studio to really make a mess of things.

With any luck I’ll have a another post soon explaining how to mitigate these issues.


Here are some of the pages I referenced while creating this. Thanks, guys!