Originally published September 9, 2019 @ 2:23 am

Here’s a typical scenario: I go to a birthday party where everyone knows I’m a shutterbug, so I have to bring my camera. As time goes by and blood alcohol concentration rises and attention to detail declines, the photos become increasingly blurry. The trick is to take a lot of them and then, hopefully, some will come out well. But which ones?

Going through hundreds (or, since I got a mirrorless camera – thousands) shots the next morning – while nursing a hangover – is not a fun way to spend one’s morning, or afternoon. But it has to be done, as friends are getting impatient about seeing photographic evidence of all the shennanigans that transpired the night before.

I don’t really need or want to look at all the photos I took. At this point I just want to concentrate my attention on the probably very few that are in-focus – I don’t even care about their artistic merits. But finding them is a tedious process.

It then occurred to me (well after the same thought occurred to many other much smarter people) that ImageMagick is a great tool for detecting blurry photos. And so I decided to give it a shot.

Some important considerations

ImageMagick – unless restricted by its policy config file – will use up as much oomph as your computer can provide. And analyzing high-resolution photos taken my modern cameras requires a lot of system resources. Unless you can wait, you should run this process on a serious multi-core system with lots of RAM. And good network performance, if your photos reside on a network-mounted share. If this is a physical computer, make sure it is being properly cooled.

My setup

For everything below I was using a Dell XPS-15 with Windows 10, WSL2 running Ubuntu 18.04 LTS. The photos were located on a CIFS-mounted share on a QNAP NAS. And the network is 5GHz WiFi.

Mounting NAS share

This took a little bit to figure out, but the easiest way to mount a CIFS share on your WLSL Linux instance is to first mount it in Windows as a letter drive (W:, in my case) and the mount it in Linux:

mount_point="/mnt/nas04/backups"
mkdir -p "${mount_point}"
echo -e "W:\t${mount_point}\tdrvfs"
mount "${mount_point}"

# And create an easy-to-use link to wherever your photos are
ln -s "${mount_point}/photos/2019-09-08 Bobs Bday Party" ${HOME}/bobs_bday_2019

Basic CLI syntax for mounting a network share under WSL Linux is this:

mount -t drvfs '\server\share' /mnt/share

However, at the moment there is no way to pass credentials, meaning you’ll get ‘access denied’ unless your current Windows account has access to the share.

Installing/upgrading ImageMagick

This may require a bit of a personal touch, depending on the condition of your WSL Ubuntu, or whatnot, but the general process for me was this. And it will take some time to run. Being a sysadmin, I do most things under root ID, which is a bad idea, but here we are. So just add sudo wherever appropriate.

# Install some pre-reqs
apt-get install build-essential checkinstall \
libx11-dev libxext-dev zlib1g-dev libpng12-dev \
libjpeg-dev libfreetype6-dev libxml2-dev
 
# Enable source code repos
sed -i '/deb-src/s/^# //' /etc/apt/sources.list && apt update
 
# Install some more pre-reqs
apt-get build-dep imagemagick
 
# Make build dirs
mkdir $HOME/imagemagick_build && cd $HOME/imagemagick_build
 
# Install from source. This will take a while
v="$(curl -s0 -k https://www.imagemagick.org/download/ | grep -oP "(?<=href=\")ImageMagick-[0-9]\.[0-9]\.[0-9]-[0-9]{1,}\.tar\.bz2(?=\">Image)" | sort -V | tail -1)"
v1="$(echo $v | egrep -o '[0-9]\.[0-9]\.[0-9]-[0-9]{1,}')"
wget https://www.imagemagick.org/download/${v} && \
tar xvf ${v} && cd ${v} && ./configure && make && \
sudo checkinstall -D --install=yes --fstrans=no --pakdir "$HOME/imagemagick_build" \
--pkgname imagemagick --backup=no --deldoc=yes --deldesc=yes --delspec=yes --default \
--pkgversion "${v1}" && \
make distclean && sudo ldconfig
 
# Check version
identify -version

Identify blurry photos

The process below will not tell you definitively if a photo is blurry or not. But it will assign a four-digit number to each photo: the smaller the number, the more likely it is that the photo is blurry. You can then sort your photos and fairly quickly figure out a good cutoff point where the photos are very likely blurry and you should concentrate your attention elsewhere.

This little script will analyze all photos in the current folder and copy them to a subfolder with new filenames reflecting the “bluriness index”. Naturally, it would’ve been better if I could soft-link instead of copying, but WSL currently does not support soft or hard links. Microsoft is working on this, though.

mkdir -p ./analyzed 
find . -maxdepth 1 -mindepth 1 -regextype posix-extended -regex '^.*\.[jJ][pP]([eE])?[gG]' -printf '%f\n' | while read i; do
b="$(magick "${i}" -statistic StandardDeviation 5X5 -format "%[fx:maxima]\n" info: | awk -F'.' '{print $2}' | cut -c 1-4)"
echo "Copying ${i} to ./analyzed/${b}_${i}"
cp -p ./${i} ./analyzed/${b}_${i}
done

The output should look something like this:

Copying DSCF2854.JPG to ./analyzed/2975_DSCF2854.JPG
Copying DSCF2855.JPG to ./analyzed/2241_DSCF2855.JPG
Copying DSCF2856.JPG to ./analyzed/2311_DSCF2856.JPG
Copying DSCF2857.JPG to ./analyzed/1429_DSCF2857.JPG
Copying DSCF2858.JPG to ./analyzed/1109_DSCF2858.JPG
Copying DSCF2859.JPG to ./analyzed/4200_DSCF2859.JPG
Copying DSCF2860.JPG to ./analyzed/4123_DSCF2860.JPG

From here you can easily tell that 1109_DSCF2858.JPG is definitely a lot more blurry than 4200_DSCF2859.JPG.

File-naming convention considerations

In the example above, I prepend the “bluriness index” to the original filename. This makes it easy to identify photos that are most likely to be blurry and, therefore, ignore them for the time being.

Having said that, most photos are taken in a sequence: when a bunch of friends are posing for a shot, you will not take just one photo, right? From a practical standpoint, perhaps it would make it more sense to append the “bluriness index” to the original filename. This really depends on your workflow preferences. But here’s an example anyway:

mkdir -p ./analyzed 
find . -maxdepth 1 -mindepth 1 -regextype posix-extended -regex '^.*\.[jJ][pP]([eE])?[gG]' -printf '%f\n' | while read i; do
b="$(magick "${i}" -statistic StandardDeviation 5X5 -format "%[fx:maxima]\n" info: | awk -F'.' '{print $2}' | cut -c 1-4)"
e="$(echo ${i} | awk -F'.' '{print $NF}')"
f="$(echo ${i} | awk -F'.' '{$NF=""; print $0}'"
echo "Copying ${i} to ./analyzed/${f}_${b}.${e}"
cp -p ./${i} ./analyzed/${f}_${b}.${e}
done

Making the process faster

There are a couple of ways you can make this process run faster. One option is to replace 5x5 in the command above with lower values, like 3x3. Another way is to create copies of your original photos with lower resolution (this will also save you on bandwidth).

Quality control considerations

Obviously, this process is not perfect. The statistical methods used by ImageMagick – or, rather, my assumptions when interpreting the results – can be skewed by certain photos where a large background are is blurred and a relatively small subject is in focus. A good example would be a wide-aperture shot – say, a portrait – where the subject’s face is in focus, while most of the background is blurry. Do watch out for those.

When you have two photos of the same scene at the same exposure, the sharpest one will have the highest standard deviation. The issue here, of course, is that we’re trying to analyze a variety of rather dissimilar photos. The end result, therefore, is more of a quick guide and not a definitive metrics.

Plan B

There are many other ways to approach this issue of detecting relative sharpness of dissimilar photos. One possibility is to do Canny Edge Detection. Here’s a quick example:

# Run auto-levels
convert imagejpg -auto-level image.png

# Do Canny Edge Detection
convert image.png -canny 0x1+10%+30% image_canny10x30.png

# And now just compare the resulting images using the previously-described method.

Depending on the specifics of your photos, inevitably you may run into the same problem: you’re trying to analyze and compare very dissimilar photos. So what if photo01.jpg has less detail that photo23.jpg? Maybe photo01.jpg had intentionally shallow focal zone and photo23.jpg is just out of focus.

Plan C

No matter how you slice it, the key here is to compare similar photos – the same scene taken at the same exposure. So how do you identify those? The simplest option is by timestamp. If you went “click-click-click”, the resulting files will have similar or even identical timestamp. If you group those together into “scenes” and analyze each “scene” separately, the results will be much more accurate.

So, the first step would be to separate all the photos into “scenes”. For this example I will define a “scene” as any number of photos taken no more than five seconds apart:

f=$(mktemp)
thr=5
loop=1
scene=1
folder="./scenes/scene_$(printf "%010d" $scene)"
mkdir -p "${folder}"
echo "Processing scene ${scene}"
find . -maxdepth 1 -mindepth 1 -regextype posix-extended -regex '^.*\.[jJ][pP]([eE])?[gG]' -printf '%f\n' | sort -n | while read i; do
  if [ ${loop} -eq 1 ]
  then 
    /bin/cp -p "${i}" ./${folder}/
    date -d "$(identify -format '%[EXIF:DateTime]' "${i}" | sed 's/:/-/g; s/-/:/3g')" +'%s' > ${f}
    (( loop = loop + 1 ))
  else
    d1="$(cat ${f})"
    d2="$(date -d "$(identify -format '%[EXIF:DateTime]' "${i}" | sed 's/:/-/g; s/-/:/3g')" +'%s')"
    echo ${d2} > "${f}"
    if ((d2 - d1 <= thr))
    then
      /bin/cp -p "${i}" ./${folder}/
    else
      (( scene = scene + 1 ))
      echo "Processing scene ${scene}"
      folder="./scenes/scene_$(printf "%010d" $scene)"
      mkdir -p "${folder}"
      /bin/cp -p "${i}" ./${folder}/
      date -d "$(identify -format '%[EXIF:DateTime]' "${i}" | sed 's/:/-/g; s/-/:/3g')" +'%s' > ${f}
    fi
  fi
done
/bin/rm ${f}

The next step is to analyze photos in each “scene” to determine their relative sharpness:

ls ./scenes | while read scene
do
  if [ $(ls ./scenes/${scene} | wc -l) -gt 1 ]
  then
    mkdir -p ./scenes/${scene}/analyzed 
    find ./scenes/${scene}  -maxdepth 1 -mindepth 1 -regextype posix-extended -regex '^.*\.[jJ][pP]([eE])?[gG]' -printf '%f\n' | while read i
    do
      b="$(convert "./scenes/${scene}/${i}" -statistic StandardDeviation 5X5 -format "%[fx:maxima]\n" info: | awk -F'.' '{print $2}' | cut -c 1-4)"
      echo "Copying ${i} to ./scenes/${scene}/analyzed/${b}_${i}"
      cp -p ./scenes/${scene}/${i} ./scenes/${scene}/analyzed/${b}_${i}
    done
  fi
done

The final step is to grab the least blurry photo from each “scene” (or the only photo, if the “scene” contains just one image) and copy it to the “selected” folder:

mkdir -p ./selected
ls ./scenes | while read scene
do
  if [ -d ./scenes/${scene}/analyzed ]
  then
    /bin/cp -p ./scenes/${scene}/analyzed/$(ls ./scenes/${scene}/analyzed | sort -rn | head -1) ./selected/
  else
    /bin/cp -p $(ls ./scenes/${scene} | sort -rn | head -1) ./selected/
  fi
done

Conclusion

None of these methods are perfect. But, when you have an overwhelming amount of photos and no time to visually inspect every one of them (and you’re hungover, and your equally-hungover friends demand the results), this is definitely good enough.

Whatever ends up in the “selected” folder, I just ran through a Lightroom batch job geared toward the “drunken birthday party at an obnoxious Russian restaurant” scenario and shared the results to nearly-universal acclaim. Having said that, I think I’m a better sysadmin than a photographer…

blank
At this point I was not qualified to stand up, let alone operate a camera…