Most digital cameras use some sort of naming scheme that leaves a lot to be desired. The names usually consist of something like:

• picture001.jpg
• picture002.jpg
• ...
• picture 134.jpg

As you can see that naming scheme tells you nothing about the picture. Personally I like to rename the picture based on the date and time it was taken. For example: 2010-04-04T07h35m39.jpg. With a name like that you can clearly see that the picture was taken on April 4, 2010 at 7:35 am. The neat thing about this is that all modern digital cameras write this information to what is called an EXIF[1] tag contained within the picture itself.

I wrote a python script that copies all of the pictures from a digital camera (well from the directory that is mounted in the file system) to a temporary location and renames them based on the date and time the pictures were taken. In addition it can also add some additional information to the IPTC[2] tags of the photograph.

Features:

• Reads a configuration file that contains:
• Photographer name
• Output path - the directory to copy the pictures to. Typically it is a temporary location. I would then copy the pictures manually to the final spot to ensure that nothing is accidentally over written
• Can deal with multiple configuration files and allows the user to choose which one to apply
• Searches the camera for all picture files (jpg, jpeg, png)
• Pictures are copied to the output path and renamed based on the EXIF date and time and the IPTC tags are updated as well
• Pictures are also sorted into directories based on year and month the picture was taken
• If for some reason two pictures have the exact EXIF date and time a number is appended to the file name
• After the pictures are copied and renamed, the pictures can be deleted from the camera
• Any non-picture files are displayed at the end. Useful if you have movies stored on the camera

Here is an example of the configuration file - photographer.cfg:

    :number-lines:

[camera.profile]
photographer=Troy Williams
outputpath=/home/troy/repositories/code/Python/camera copy/output/Troy Williams


Here is the script - camera_copy.py:


#!/usr/bin/env python
#-*- coding:utf-8 -*-

"""
This script copies pictures from one folder to another. It attempts to rename
the pictures based on the exif date taken tag. The script also reads from a
configuration file that contains, amoung other things, the name of the
photographer (which is assigned to the photographer IPTC tag) as well as the
folder to copy the images to.

Documentation:
-Contains urls to sites containing relevant documentation for the code in
in question. Normally this should be inlined closed to the code where it
is used.

References:
-Contains links to reference materials used. If specific functions are used
directly, then credit is placed there

Dependencies:
pyexiv2 - http://tilloy.net/dev/pyexiv2/index.htm
http://tilloy.net/dev/pyexiv2/tutorial.htm

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""

import sys
import os
import shutil
from datetime import datetime
import pyexiv2

#Constants
__uuid__ = 'f706d95a-6c94-4a1e-ab4c-a8ee26b0c563'
__version__ = '0.2'
__author__ = 'Troy Williams'
__email__ = 'troy.williams@bluebill.net'
__date__ = '2010-04-10'
__maintainer__ = 'Troy Williams'
__status__ = 'Development'

def confirm(prompt=None, resp=False):
"""
Source: http://code.activestate.com/recipes/541096/

prompts for yes or no response from the user. Returns True for yes and
False for no.

'resp' should be set to the default value assumed by the caller when
user simply types ENTER.

&gt;&gt;&gt; confirm(prompt='Create Directory?', resp=True)
Create Directory? [y]|n:
True
&gt;&gt;&gt; confirm(prompt='Create Directory?', resp=False)
Create Directory? [n]|y:
False
&gt;&gt;&gt; confirm(prompt='Create Directory?', resp=False)
Create Directory? [n]|y: y
True

TBW: 2009-11-13 - change the prompt if test
"""

if not prompt:
prompt = 'Confirm'

if resp:
prompt = '%s [%s]|%s: ' % (prompt, 'y', 'n')
else:
prompt = '%s [%s]|%s: ' % (prompt, 'n', 'y')

while True:
ans = raw_input(prompt)
if not ans:
return resp
if ans not in ['y', 'Y', 'n', 'N']:
print 'please enter y or n.'
continue
if ans == 'y' or ans == 'Y':
return True
if ans == 'n' or ans == 'N':
return False

def process_command_line():
"""
Sets up the command line options and arguments
"""
from optparse import OptionParser

usage = """
usage: %prog [options] path1 path2 path3

The program takes a path (or number of paths) to the directory where
the pictures are stored. The paths can be relative to the current
script location. It takes the pictures and copies them to a location
based on the configuration settings and renames them based on the
exif date stored within the image. In addition the images will be
sorted into directories based on the exif date. They are sorted by
year and month.

In the same folder as the script, configuration files are detected
and the user is prompted to select one. A configuration file can
contain the following:

[Camera.Profile]
photographer=Troy Williams
outputpath=/home/troy/Pictures/Troy Williams

The configuration file must contain the [Camera.Profile] header

photographer - The name of the person that took the pictures
of the photo
outputpath - The path to copy the pictures too. They will be sorted
by year/month based on the exif information stored in the picture.
If no information is available it will be placed into a misc
directory.
"""
parser = OptionParser(usage=usage, version='%prog v' + __version__)

options, args = parser.parse_args(args=None, values=None)

if not args:
parser.error('At least one image path must be specified!')
parser.print_help()

return options, args

def find(path, pattern=None):
"""
Takes a path and recursively finds the files.

Optionally pattern can be specified where pattern = '*.txt' or something
that fnmatch would find useful

NOTE: this is a generator and should be used accordingly
"""

if not os.path.exists(path):
raise Exception, '%s does not exist!' % path

if pattern:
#search for the files that match the specific pattern
import fnmatch
for root, dirnames, filenames in os.walk(path):
for filename in fnmatch.filter(filenames, pattern):
yield os.path.join(root, filename)
else:
#search for all files
for root, dirnames, filenames in os.walk(path):
for filename in filenames:
yield os.path.join(root, filename)

def make_directory(dir_path):
"""
Takes the passed directory path and attempts to create it including all
directories or sub-directories that do not exist on the path.
"""

try:
os.makedirs(dir_path)
except OSError:
#Check to see if the directory already exists
if os.path.exists(dir_path):
#It exists so ignore the exception
pass
else:
#There was some other error
raise

def path_from_date(path, date):
"""
Takes a path and a date. It extracts the year and month from the date and
returns a new path

path = /home/troy/picture

date = 2010-04-03 12:22:12 PM

returns a path like /home/troy/picture/2010/03
"""

return os.path.join(path, str(date.year), date.strftime("%m"))

"""
Takes a path to a configuration file and reads in the values stored there.

Returns: dictionary
"""

if not os.path.exists(path):
raise Exception, '%s does not exist!' % path

import ConfigParser

#Set the defaults
configParams = {}
configParams['photographer'] = None
configParams['output_path'] = None
configParams['extensions'] = ['.jpg', '.jpeg', '.JPEG', '.JPG', '.png']

config = ConfigParser.RawConfigParser()

#loop through all the items in the section and assign the values to the the
#configParams dictionary... We don't assign it as the default dictionary
#because, the options we are interested in are defined above... This
#appears to be case sensitive therefore we make the keys lower case
for name, value in config.items('camera.profile'):
configParams[name.lower()] = value

return configParams

def suggest_file_name(path):
"""
Takes a file path and checks to see if the file exists at that location. If
it doesn't then it simply returns the path unchanged. If the path exists, it
will attempt generate a new file name and check to see if it exists.

If a new name is found, it is returned.
If the original name is not duplicated, it is returned
If the looping limit is reached, None is returned
"""

if os.path.lexists(path):
filename, extension = os.path.splitext(path)
for i in xrange(1, 1000):
#Suggest a new file name of the form "file_name (1).jpg"
newFile = '%s (%d)%s' % (filename, i, extension)
if not os.path.lexists(newFile):
return newFile
return None
else:
return path

def update_image_iptc(path, **iptc):
"""
This takes an image and updates the iptc information based on the passed
parameters
"""

if not os.path.exists(path):
raise Exception, '%s does not exist!' % path

if 'exifDateTime' in iptc:
image['Iptc.Application2.DateCreated'] = [iptc['exifDateTime']]

if 'photographer' in iptc:
image['Iptc.Application2.Byline'] = [iptc['photographer']]
image['Iptc.Application2.Writer'] = [iptc['photographer']]

image.write()

def main():
"""
The heart of the script. Takes all of the bits and organizes them into a
proper program
"""
#grab the command line arguments
options, args = process_command_line()

#grab the path to the script.
scriptPath = sys.path[0]

#Search the scriptPath for configuration files
configurationFiles = []
for filename in find(scriptPath, pattern='*.cfg'):
configurationFiles.append(filename)

#make sure that there is at least one configuration file
if not configurationFiles:
raise Exception, 'No configurations files found!'

print 'Please choose the number of the configuration file to use:'

for i, item in enumerate(configurationFiles):
print '%i : %s' % (i, os.path.basename(item))

#prompt the user to pick the index of the configuration file to execute
index = int(raw_input("Choose the configuration: "))
selectedConfiguration = configurationFiles[index]

print "Configuration file: ", selectedConfiguration

#make the root output directory
make_directory(configParams['outputpath'])

#Store a list of files that were successfully copied for later deletion
matches = []

#Store a list of files that were not in configParams['extensions'] but in
#the search path
mismatches = []

#potential files to delete
to_delete = []

#copy all of the pictures from the specified paths
for picture_path in args:
normpath = os.path.join(scriptPath, picture_path)
print "Searching ", normpath
for filename in find(normpath):
filebasename, fileextension = os.path.splitext(filename)
if fileextension in configParams['extensions']:
#record the matched file for later statistics
matches.append(filename)
else:
#record the mismatch and continue the loop
mismatches.append(filename)
continue

print 'Attempting to copy: ' + os.path.basename(filename)

if 'Exif.Image.DateTime' in image.exif_keys:
#rename the file based on the exif date and time and copy the
#picture to a folder based on year/month

exifDateTime = image['Exif.Image.DateTime'].value
newpath = path_from_date(configParams['outputpath'],
exifDateTime)
make_directory(newpath)

newFile = exifDateTime.strftime("%Y-%m-%dT%Hh%Mm%S") + fileextension
newpath = os.path.join(newpath, newFile)
else:
#no exif date time tag, simply copy to the unsorted directory
#exifDateTime = datetime.strftime("%Y-%m-%dT%Hh%Mm%S")
exifDateTime = datetime.today()
newpath = os.path.join(configParams['outputpath'], "unsorted")
make_directory(newpath)

newpath = os.path.join(newpath, os.path.basename(filename))

#check to see if there are any duplicate file names
newpath = suggest_file_name(newpath)
if not newpath:
print 'Too many duplicates for: ' + filename
continue

shutil.copy2(filename, newpath)

update_image_iptc(newpath, exifDateTime=exifDateTime,
photographer=configParams['photographer'],

#The file has been successfully copied, add it to the list of files
#delete
to_delete.append(filename)

#check to see if there are any files to delete
if len(to_delete) &gt; 0:
#prompt the user if they want to delete the files
if confirm(prompt='Delete %s files?' % len(to_delete), resp=False):
deletedCount = 0
for item in to_delete:
os.remove(item)

#print out the list of invalid files - if any
if len(mismatches) &gt; 0:
print "Files not in valid extension list:"
for item in mismatches:
print item

return 0 # success

if __name__ == '__main__':
status = main()
sys.exit(status)


Here is an example of a shell script configured for a particular camera - camera.sh:

    :number-lines:

#!/bin/bash
./camera_copy.py /media/FC30-3DA9