Copy Pictures from a Digital Camera and Automatically Rename to Date and Time Taken

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 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 tags of the photograph.

Features:

  • Reads a configuration file that contains:
    • Photographer name
    • Copyright notice
    • 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:

[camera.profile]
photographer=Troy Williams
copyright=Copyright 2010 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

License:
The MIT License

Copyright (c) 2010 Troy Williams

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'
__copyright__ = 'Copyright (c) 2010, Troy Williams'
__date__ = '2010-04-10'
__license__ = 'MIT'
__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.

    >>> confirm(prompt='Create Directory?', resp=True)
    Create Directory? [y]|n:
    True
    >>> confirm(prompt='Create Directory?', resp=False)
    Create Directory? [n]|y:
    False
    >>> 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
            copyright=Copyright 2010 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
            copyright - a string that will be added to the IPTC copyright tag
            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"))

def loadConfigParameters(path):
    """
    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['copyright'] = None
    configParams['output_path'] = None
    configParams['extensions'] = ['.jpg', '.jpeg', '.JPEG', '.JPG', '.png']

    config = ConfigParser.RawConfigParser()
    config.read(path)

    #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

    image = pyexiv2.ImageMetadata(path)
    image.read()

    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']]

    if 'copyright' in iptc:
        image['Iptc.Application2.Copyright'] = [iptc['copyright']]

    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

    #load the configuration file parameters
    configParams = loadConfigParameters(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)

            image = pyexiv2.ImageMetadata(filename)
            image.read()

            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'],
                                       copyright=configParams['copyright'])

            #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) > 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) > 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:

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

2 thoughts on “Copy Pictures from a Digital Camera and Automatically Rename to Date and Time Taken

  1. Christopher Steel says:

    Great script! Works like a charm on ubuntu lynx after running ‘sudo apt-get install python-pyexiv2’. Sweet! Thank You!

  2. I found a small bug in the script when the pictures don’t have exif or iptc information. It is corrected and updated in the post.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s