fix style

This commit is contained in:
Antonio J. Delgado 2023-09-10 15:13:17 +03:00
parent 1186a66aba
commit 232a48faef

View file

@ -21,7 +21,8 @@ from datetime import datetime
class CustomFormatter(logging.Formatter): class CustomFormatter(logging.Formatter):
"""Logging colored formatter, adapted from https://stackoverflow.com/a/56944256/3638629""" """Logging colored formatter, adapted from
https://stackoverflow.com/a/56944256/3638629"""
grey = '\x1b[38;21m' grey = '\x1b[38;21m'
blue = '\x1b[38;5;39m' blue = '\x1b[38;5;39m'
@ -49,12 +50,15 @@ class CustomFormatter(logging.Formatter):
class image_classifier: class image_classifier:
def __init__(self, debug_level, log_file, faces_directory, directory, no_move, def __init__(self, debug_level, log_file, faces_directory, directory,
people_folder, recursive, folder_date_format): no_move, people_folder, recursive, folder_date_format):
''' Initial function called when object is created ''' ''' Initial function called when object is created '''
self.debug_level = debug_level self.debug_level = debug_level
if log_file is None: if log_file is None:
home_path = os.environ.get('HOME', os.environ.get('USERPROFILE', os.getcwd())) home_path = os.environ.get(
'HOME',
os.environ.get('USERPROFILE', os.getcwd())
)
log_file = os.path.join(home_path, 'log', 'image_classifier.log') log_file = os.path.join(home_path, 'log', 'image_classifier.log')
self.log_file = log_file self.log_file = log_file
self._init_log() self._init_log()
@ -97,17 +101,20 @@ class image_classifier:
files.append(file) files.append(file)
elif file.is_dir(follow_symlinks=False): elif file.is_dir(follow_symlinks=False):
more_files = self.recursive_scandir( more_files = self.recursive_scandir(
file.path, file.path,
ignore_hidden_files=ignore_hidden_files ignore_hidden_files=ignore_hidden_files
) )
if more_files: if more_files:
files = files + more_files files = files + more_files
except PermissionError as error: except PermissionError as error:
self._log.warning(f"Permission denied accessing folder '{path}'. {error}") self._log.warning(
f"Permission denied accessing folder '{path}'. {error}"
)
return files return files
def process_metadata(self, file): def process_metadata(self, file):
''' Process the metadata of an image file and store it in self.metadata''' ''' Process the metadata of an image file and store
it in self.metadata'''
self.metadata = pyexiv2.ImageMetadata(file) self.metadata = pyexiv2.ImageMetadata(file)
self.metadata.read() self.metadata.read()
if 'Xmp.iptcExt.PersonInImage' in self.metadata.xmp_keys: if 'Xmp.iptcExt.PersonInImage' in self.metadata.xmp_keys:
@ -116,8 +123,9 @@ class image_classifier:
(type: {type(self.metadata['Xmp.iptcExt.PersonInImage'].raw_value)})") (type: {type(self.metadata['Xmp.iptcExt.PersonInImage'].raw_value)})")
def get_file_date(self, file): def get_file_date(self, file):
''' Obtain the file date from EXIF metadata or the file name. Return None ''' Obtain the file date from EXIF metadata or the file name. Return
if it's not accessible or 'unknown-time' if it can't determine the date''' None if it's not accessible or 'unknown-time' if it can't determine the
date'''
file_date = None file_date = None
# dirname = os.path.dirname(os.path.realpath(file)) # dirname = os.path.dirname(os.path.realpath(file))
filename = os.path.basename(file) filename = os.path.basename(file)
@ -126,34 +134,50 @@ class image_classifier:
else: else:
if self.is_image(file): if self.is_image(file):
if 'Exif.Photo.DateTimeOriginal' in self.metadata.exif_keys: if 'Exif.Photo.DateTimeOriginal' in self.metadata.exif_keys:
original_date = self.metadata['Exif.Photo.DateTimeOriginal'].value original_date = self.metadata[
self._log.debug(f"File creation time in EXIF: {original_date} \ 'Exif.Photo.DateTimeOriginal'
(type: {type(original_date)})") ].value
self._log.debug(
f"File creation time in EXIF: {original_date} \
(type: {type(original_date)})"
)
try: try:
# file_date = original_date.strftime('%Y/%Y.%m.%d') # file_date = original_date.strftime('%Y/%Y.%m.%d')
file_date = original_date file_date = original_date
except Exception as error: except Exception as error:
self._log.error(f"Failed to convert EXIF information about date '{original_date}'. {error}") self._log.error(
f"Failed to convert EXIF information about date \
'{original_date}'. {error}"
)
file_date = None file_date = None
if file_date is None: if file_date is None:
self._log.debug('Date not stored in EXIF metadata') self._log.debug('Date not stored in EXIF metadata')
match = re.search( match = re.search(
r'(?P<year>20[0-9]{2})[\-/\._]?(?P<month>[0-1]?[0-9])[\-/\._]?(?P<day>[0-3]?[0-9])', r'(?P<year>20[0-9]{2})[\-/\._]?(?P<month>[0-1]?[0-9])\
[\-/\._]?(?P<day>[0-3]?[0-9])',
filename filename
) )
if match: if match:
file_date = datetime.strptime(f"{match.group('year')}.{match.group('month')}.\ file_date = datetime.strptime(
{match.group('day')}", '%Y.%m.%d') f"{match.group('year')}.{match.group('month')}.\
{match.group('day')}", '%Y.%m.%d'
)
else: else:
match = re.search(r'(?P<day>[0-3]?[0-9])[\-/\._]?\ match = re.search(r'(?P<day>[0-3]?[0-9])[\-/\._]?\
(?P<month>[0-1]?[0-9])[\-/\._]?(?P<year>20[0-9]{2})', filename) (?P<month>[0-1]?[0-9])[\-/\._]?(?P<year>20[0-9]{2})', filename)
if match: if match:
file_date = datetime.strptime(f"{match.group('year')}.{match.group('month')}.\ file_date = datetime.strptime(
{match.group('day')}", '%Y.%m.%d') f"{match.group('year')}.{match.group('month')}.\
{match.group('day')}", '%Y.%m.%d'
)
else: else:
self._log.warning(f"Date not found in file's name '{filename}'.") self._log.warning(
f"Date not found in file's name '{filename}'."
)
else: else:
self._log.debug(f"The file '{file}' doesn't seem to be an image for PIL.") self._log.debug(
f"The file '{file}' doesn't seem to be an image for PIL."
)
self._log.debug(f"Time based folder name section '{file_date}'") self._log.debug(f"Time based folder name section '{file_date}'")
return file_date return file_date
@ -174,7 +198,9 @@ class image_classifier:
if self.is_image(file): if self.is_image(file):
people = self.find_faces(file) people = self.find_faces(file)
if people: if people:
self._log.debug(f"Found {len(people)} known people in the image.") self._log.debug(
f"Found {len(people)} known people in the image."
)
self._log.debug(json.dumps(people, indent=2)) self._log.debug(json.dumps(people, indent=2))
self.append_people(file, people) self.append_people(file, people)
@ -184,28 +210,41 @@ class image_classifier:
os.makedirs(new_path) os.makedirs(new_path)
if self.people_folder: if self.people_folder:
for person in people: for person in people:
person_path = os.path.join(self.people_folder, person_path = os.path.join(
person.replace(' ', '.'), self.people_folder,
folder_date) person.replace(' ', '.'),
folder_date
)
if not os.path.exists(person_path): if not os.path.exists(person_path):
os.makedirs(person_path) os.makedirs(person_path)
self._log.info(f"Copying file '{file}' to person '{person}' folder,\ self._log.info(
'{person_path}'...") f"Copying file '{file}' to person '{person}' \
folder, '{person_path}'...")
try: try:
shutil.copy(file, person_path) shutil.copy(file, person_path)
except FileNotFoundError as error: except FileNotFoundError as error:
self._log.error(f"Error copying. File not found. {error}") self._log.error(
f"Error copying. File not found. {error}"
)
if not self.no_move: if not self.no_move:
if os.path.exists(os.path.join(new_path, filename)): if os.path.exists(os.path.join(new_path, filename)):
self._log.debug(f"Destination '{new_path}/{filename}' exists, removing it...") self._log.debug(
f"Destination '{new_path}/{filename}' exists, \
removing it..."
)
os.remove(os.path.join(new_path, filename)) os.remove(os.path.join(new_path, filename))
self._log.info(f"Moving file '{file}' to '{new_path}'...") self._log.info(f"Moving file '{file}' to '{new_path}'...")
try: try:
shutil.move(file, new_path) shutil.move(file, new_path)
except FileNotFoundError as error: except FileNotFoundError as error:
self._log.error(f"Error copying. File not found. {error}") self._log.error(
f"Error copying. File not found. {error}"
)
else: else:
self._log.info(f"NOT moving file '{file}' to '{new_path}' because of --no-move") self._log.info(
f"NOT moving file '{file}' to '{new_path}' because of \
--no-move"
)
def print_metadata(self): def print_metadata(self):
print("IPTC keys:") print("IPTC keys:")
@ -231,8 +270,10 @@ class image_classifier:
if 'Xmp.iptcExt.PersonInImage' in self.metadata.xmp_keys: if 'Xmp.iptcExt.PersonInImage' in self.metadata.xmp_keys:
self.metadata['Xmp.iptcExt.PersonInImage'].value = new_list self.metadata['Xmp.iptcExt.PersonInImage'].value = new_list
else: else:
self.metadata['Xmp.iptcExt.PersonInImage'] = pyexiv2.XmpTag('Xmp.iptcExt.PersonInImage', self.metadata['Xmp.iptcExt.PersonInImage'] = pyexiv2.XmpTag(
new_list) 'Xmp.iptcExt.PersonInImage',
new_list
)
self._log.debug(f"People (after): \ self._log.debug(f"People (after): \
{self.metadata['Xmp.iptcExt.PersonInImage'].raw_value} \ {self.metadata['Xmp.iptcExt.PersonInImage'].raw_value} \
(type: {type(self.metadata['Xmp.iptcExt.PersonInImage'].raw_value)})") (type: {type(self.metadata['Xmp.iptcExt.PersonInImage'].raw_value)})")
@ -252,24 +293,37 @@ class image_classifier:
def load_known_people(self): def load_known_people(self):
'''Load faces of known people using the file names as person name''' '''Load faces of known people using the file names as person name'''
known_people = list() known_people = list()
self._log.debug(f"Looking for known faces in directory '{self.faces_directory}'...") self._log.debug(f"Looking for known faces in directory \
'{self.faces_directory}'...")
if os.access(self.faces_directory, os.R_OK): if os.access(self.faces_directory, os.R_OK):
with os.scandir(self.faces_directory) as faces_items: with os.scandir(self.faces_directory) as faces_items:
for entry in faces_items: for entry in faces_items:
if (not entry.name.startswith('.') and entry.is_file() and if (not entry.name.startswith('.') and entry.is_file() and
self.is_image(self.faces_directory + os.sep + entry.name)): self.is_image(
self._log.debug(f"Identifying face in file '{entry.name}'...") self.faces_directory + os.sep + entry.name
)):
self._log.debug(
f"Identifying face in file '{entry.name}'..."
)
person = dict() person = dict()
person['filename'] = face_recognition.load_image_file(self.faces_directory + person['filename'] = face_recognition.load_image_file(
os.sep + entry.name) self.faces_directory + os.sep + entry.name
person['name'] = os.path.basename(os.path.splitext(self.faces_directory + )
os.sep + entry.name)[0]) person['name'] = os.path.basename(
encodings = face_recognition.face_encodings(person['filename']) os.path.splitext(
self.faces_directory + os.sep + entry.name
)[0]
)
encodings = face_recognition.face_encodings(
person['filename']
)
if len(encodings) > 0: if len(encodings) > 0:
person['encoding'] = encodings[0] person['encoding'] = encodings[0]
known_people.append(person) known_people.append(person)
else: else:
self._log.info(f"No faces found in file '{entry.name}'.") self._log.info(
f"No faces found in file '{entry.name}'."
)
return known_people return known_people
def find_faces(self, file): def find_faces(self, file):
@ -281,7 +335,9 @@ class image_classifier:
self._log.debug(f"Found {len(encodings)} faces.") self._log.debug(f"Found {len(encodings)} faces.")
for known_person in self.known_people: for known_person in self.known_people:
for encoding in encodings: for encoding in encodings:
if face_recognition.compare_faces([known_person['encoding']], encoding)[0]: if face_recognition.compare_faces(
[known_person['encoding']], encoding
)[0]:
if known_person['name'] not in people: if known_person['name'] not in people:
people.append(known_person['name']) people.append(known_person['name'])
else: else:
@ -295,7 +351,9 @@ class image_classifier:
self._log.debug(f"File '{file}' is not readable by PIL. {error}") self._log.debug(f"File '{file}' is not readable by PIL. {error}")
return False return False
except PIL.UnidentifiedImageError as error: except PIL.UnidentifiedImageError as error:
self._log.debug(f"File '{file}' is not an image recognizable by PIL. {error}") self._log.debug(
f"File '{file}' is not an image recognizable by PIL. {error}"
)
return False return False
return True return True
@ -318,9 +376,14 @@ class image_classifier:
if not os.path.exists(os.path.dirname(self.log_file)): if not os.path.exists(os.path.dirname(self.log_file)):
os.mkdir(os.path.dirname(self.log_file)) os.mkdir(os.path.dirname(self.log_file))
filehandler = logging.handlers.RotatingFileHandler(self.log_file, maxBytes=102400000) filehandler = logging.handlers.RotatingFileHandler(
self.log_file,
maxBytes=102400000
)
# create formatter # create formatter
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s') formatter = logging.Formatter(
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
)
filehandler.setFormatter(formatter) filehandler.setFormatter(formatter)
filehandler.setLevel(logging.DEBUG) filehandler.setLevel(logging.DEBUG)
self._log.addHandler(filehandler) self._log.addHandler(filehandler)
@ -335,20 +398,31 @@ class image_classifier:
), ),
help='Set the debug level for the standard output.') help='Set the debug level for the standard output.')
@click.option('--log-file', '-l', help="File to store all debug messages.") @click.option('--log-file', '-l', help="File to store all debug messages.")
@click.option("--faces-directory", "-f", required=True, help="Folder containing the pictures that \ @click.option("--faces-directory", "-f", required=True, help="Folder that \
identify people. Filename would be used as the name for the person. Just one person per picture.") contains the pictures that identify people. Filename would be used as the \
@click.option("--directory", "-d", required=True, help="Folder with the pictures to classify.") name for the person. Just one person per picture.")
@click.option("--no-move", "-n", is_flag=True, help="Don't move files, just add people's tag.") @click.option("--directory", "-d", required=True, help="Folder with the \
@click.option('--people-folder', '-p', help="Define a folder for people's folders and copy \ pictures to classify.")
pictures to each person's folder. Be sure to have deduplication in the filesystem to avoid using \ @click.option("--no-move", "-n", is_flag=True, help="Don't move files, just \
too much storage.") add people's tag.")
@click.option('--recursive', '-r', is_flag=True, help='Recursively search for files in the provided --directory') @click.option('--people-folder', '-p', help="Define a folder for people's \
@click.option('--folder-date-format', '-F', default='%Y/%Y.%m.%d', help='Format for the folder with the file date according to https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior') folders and copy \
pictures to each person's folder. Be sure to have deduplication in the \
filesystem to avoid using too much storage.")
@click.option('--recursive', '-r', is_flag=True, help='Recursively search \
for files in the provided --directory')
@click.option('--folder-date-format', '-F', default='%Y/%Y.%m.%d',
help='Format for the folder with the file date according to \
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior'
)
@click_config_file.configuration_option() @click_config_file.configuration_option()
def __main__(debug_level, log_file, faces_directory, directory, no_move, def __main__(debug_level, log_file, faces_directory, directory, no_move,
people_folder, recursive, folder_date_format): people_folder, recursive, folder_date_format):
return image_classifier(debug_level, log_file, faces_directory, directory, no_move, return image_classifier(
people_folder, recursive, folder_date_format) debug_level, log_file, faces_directory, directory,
no_move, people_folder, recursive,
folder_date_format
)
if __name__ == "__main__": if __name__ == "__main__":