"""
rename_images.py
Copies images from a possibly-nested folder structure to a flat folder structure, including EXIF
timestamps in each filename. Loosely equivalent to camtrapR's imageRename() function.
"""
#%% Imports and constants
import os
import sys
import argparse
from megadetector.utils.path_utils import \
find_images, insert_before_extension, parallel_copy_files
from megadetector.data_management.read_exif import \
ReadExifOptions, read_exif_from_folder
#%% Functions
[docs]
def rename_images(input_folder,
output_folder,
dry_run=False,
verbose=False,
read_exif_options=None,
n_copy_workers=8):
"""
Copies images from a possibly-nested folder structure to a flat folder structure,
including EXIF timestamps in each filename. Loosely equivalent to camtrapR's
imageRename() function.
Args:
input_folder (str): the folder to search for images, always recursive
output_folder (str): the folder to which we will copy images; cannot be the
same as [input_folder]
dry_run (bool, optional): only map images, don't actually copy
verbose (bool, optional): enable additional debug output
read_exif_options (ReadExifOptions, optional): parameters controlling the reading of
EXIF information
n_copy_workers (int, optional): number of parallel threads to use for copying
Returns:
dict: a dict mapping relative filenames in the input folder to relative filenames in the output
folder
"""
assert os.path.isdir(input_folder), 'Input folder {} does not exist'.format(
input_folder)
if not dry_run:
os.makedirs(output_folder,exist_ok=True)
# Read exif information
if read_exif_options is None:
read_exif_options = ReadExifOptions()
read_exif_options.tags_to_include = ['DateTime','Model',
'Make','ExifImageWidth',
'ExifImageHeight','DateTimeOriginal']
read_exif_options.verbose = False
exif_info = read_exif_from_folder(input_folder=input_folder,
output_file=None,
options=read_exif_options,
filenames=None,recursive=True)
print('Read EXIF information for {} images'.format(len(exif_info)))
filename_to_exif_info = {info['file_name']:info for info in exif_info}
image_files = find_images(input_folder,return_relative_paths=True,convert_slashes=True,recursive=True)
for fn in image_files:
assert fn in filename_to_exif_info, 'No EXIF info available for {}'.format(fn)
input_fn_relative_to_output_fn_relative = {}
# fn_relative = image_files[0]
for fn_relative in image_files:
input_fn_abs = os.path.join(input_folder,fn_relative)
image_exif_info = filename_to_exif_info[fn_relative]
if 'exif_tags' in image_exif_info:
image_exif_info = image_exif_info['exif_tags']
if image_exif_info is None or \
'DateTimeOriginal' not in image_exif_info or \
image_exif_info['DateTimeOriginal'] is None:
dt_tag = 'unknown_datetime'
print('Warning: no datetime for {}'.format(fn_relative))
else:
dt_tag = str(image_exif_info['DateTimeOriginal']).replace(':','-').replace(' ','_').strip()
flat_filename = fn_relative.replace('\\','/').replace('/','_')
output_fn_relative = insert_before_extension(flat_filename,dt_tag)
input_fn_relative_to_output_fn_relative[fn_relative] = output_fn_relative
if not dry_run:
input_fn_abs_to_output_fn_abs = {}
for input_fn_relative in input_fn_relative_to_output_fn_relative:
output_fn_relative = input_fn_relative_to_output_fn_relative[input_fn_relative]
input_fn_abs = os.path.join(input_folder,input_fn_relative)
output_fn_abs = os.path.join(output_folder,output_fn_relative)
input_fn_abs_to_output_fn_abs[input_fn_abs] = output_fn_abs
parallel_copy_files(input_file_to_output_file=input_fn_abs_to_output_fn_abs,
max_workers=n_copy_workers,
use_threads=True,
overwrite=True,
verbose=verbose)
# ...if this is not a dry run
return input_fn_relative_to_output_fn_relative
# ...def rename_images()
#%% Interactive driver
if False:
pass
#%% Configure options
input_folder = r'G:\camera_traps\camera_trap_images\2018.05.04'
output_folder = r'g:\temp\rename-test-out'
dry_run = False
verbose = True
read_exif_options = ReadExifOptions()
read_exif_options.tags_to_include = ['DateTime','Model','Make',
'ExifImageWidth','ExifImageHeight',
'DateTimeOriginal']
read_exif_options.n_workers = 8
read_exif_options.verbose = verbose
n_copy_workers = 8
#%% Programmatic execution
input_fn_relative_to_output_fn_relative = rename_images(input_folder,
output_folder,
dry_run=dry_run,
verbose=verbose,
read_exif_options=read_exif_options,
n_copy_workers=n_copy_workers)
#%% Command-line driver
[docs]
def main(): # noqa
parser = argparse.ArgumentParser(
description='Copies images from a possibly-nested folder structure to a flat folder structure, ' + \
'adding datetime information from EXIF to each filename')
parser.add_argument(
'input_folder',
type=str,
help='The folder to search for images, always recursive')
parser.add_argument(
'output_folder',
type=str,
help='The folder to which we should write the flattened image structure')
parser.add_argument(
'--dry_run',
action='store_true',
help="Only map images, don't actually copy")
if len(sys.argv[1:]) == 0:
parser.print_help()
parser.exit()
args = parser.parse_args()
rename_images(args.input_folder,args.output_folder,dry_run=args.dry_run,
verbose=True,read_exif_options=None)
if __name__ == '__main__':
main()